httpd.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // Package httpd implements REST API and Web interface for SFTPGo.
  2. // The OpenAPI 3 schema for the exposed API can be found inside the source tree:
  3. // https://github.com/drakkan/sftpgo/blob/main/httpd/schema/openapi.yaml
  4. package httpd
  5. import (
  6. "fmt"
  7. "net"
  8. "net/http"
  9. "net/url"
  10. "path"
  11. "path/filepath"
  12. "runtime"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/go-chi/chi/v5"
  17. "github.com/go-chi/jwtauth/v5"
  18. "github.com/lestrrat-go/jwx/jwa"
  19. "github.com/drakkan/sftpgo/common"
  20. "github.com/drakkan/sftpgo/dataprovider"
  21. "github.com/drakkan/sftpgo/ftpd"
  22. "github.com/drakkan/sftpgo/logger"
  23. "github.com/drakkan/sftpgo/sftpd"
  24. "github.com/drakkan/sftpgo/utils"
  25. "github.com/drakkan/sftpgo/webdavd"
  26. )
  27. const (
  28. logSender = "httpd"
  29. tokenPath = "/api/v2/token"
  30. logoutPath = "/api/v2/logout"
  31. userTokenPath = "/api/v2/user/token"
  32. userLogoutPath = "/api/v2/user/logout"
  33. activeConnectionsPath = "/api/v2/connections"
  34. quotaScanPath = "/api/v2/quota-scans"
  35. quotaScanVFolderPath = "/api/v2/folder-quota-scans"
  36. userPath = "/api/v2/users"
  37. versionPath = "/api/v2/version"
  38. folderPath = "/api/v2/folders"
  39. serverStatusPath = "/api/v2/status"
  40. dumpDataPath = "/api/v2/dumpdata"
  41. loadDataPath = "/api/v2/loaddata"
  42. updateUsedQuotaPath = "/api/v2/quota-update"
  43. updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
  44. defenderBanTime = "/api/v2/defender/bantime"
  45. defenderUnban = "/api/v2/defender/unban"
  46. defenderScore = "/api/v2/defender/score"
  47. adminPath = "/api/v2/admins"
  48. adminPwdPath = "/api/v2/admin/changepwd"
  49. adminPwdCompatPath = "/api/v2/changepwd/admin"
  50. userPwdPath = "/api/v2/user/changepwd"
  51. userPublicKeysPath = "/api/v2/user/publickeys"
  52. userReadFolderPath = "/api/v2/user/folder"
  53. userGetFilePath = "/api/v2/user/file"
  54. userStreamZipPath = "/api/v2/user/streamzip"
  55. healthzPath = "/healthz"
  56. webRootPathDefault = "/"
  57. webBasePathDefault = "/web"
  58. webBasePathAdminDefault = "/web/admin"
  59. webBasePathClientDefault = "/web/client"
  60. webAdminSetupPathDefault = "/web/admin/setup"
  61. webLoginPathDefault = "/web/admin/login"
  62. webLogoutPathDefault = "/web/admin/logout"
  63. webUsersPathDefault = "/web/admin/users"
  64. webUserPathDefault = "/web/admin/user"
  65. webConnectionsPathDefault = "/web/admin/connections"
  66. webFoldersPathDefault = "/web/admin/folders"
  67. webFolderPathDefault = "/web/admin/folder"
  68. webStatusPathDefault = "/web/admin/status"
  69. webAdminsPathDefault = "/web/admin/managers"
  70. webAdminPathDefault = "/web/admin/manager"
  71. webMaintenancePathDefault = "/web/admin/maintenance"
  72. webBackupPathDefault = "/web/admin/backup"
  73. webRestorePathDefault = "/web/admin/restore"
  74. webScanVFolderPathDefault = "/web/admin/folder-quota-scans"
  75. webQuotaScanPathDefault = "/web/admin/quota-scans"
  76. webChangeAdminPwdPathDefault = "/web/admin/changepwd"
  77. webTemplateUserDefault = "/web/admin/template/user"
  78. webTemplateFolderDefault = "/web/admin/template/folder"
  79. webClientLoginPathDefault = "/web/client/login"
  80. webClientFilesPathDefault = "/web/client/files"
  81. webClientDirContentsPathDefault = "/web/client/listdir"
  82. webClientDownloadZipPathDefault = "/web/client/downloadzip"
  83. webClientCredentialsPathDefault = "/web/client/credentials"
  84. webChangeClientPwdPathDefault = "/web/client/changepwd"
  85. webChangeClientKeysPathDefault = "/web/client/managekeys"
  86. webClientLogoutPathDefault = "/web/client/logout"
  87. webStaticFilesPathDefault = "/static"
  88. // MaxRestoreSize defines the max size for the loaddata input file
  89. MaxRestoreSize = 10485760 // 10 MB
  90. maxRequestSize = 1048576 // 1MB
  91. maxLoginPostSize = 262144 // 256 KB
  92. osWindows = "windows"
  93. )
  94. var (
  95. backupsPath string
  96. certMgr *common.CertManager
  97. jwtTokensCleanupTicker *time.Ticker
  98. jwtTokensCleanupDone chan bool
  99. invalidatedJWTTokens sync.Map
  100. csrfTokenAuth *jwtauth.JWTAuth
  101. webRootPath string
  102. webBasePath string
  103. webBaseAdminPath string
  104. webBaseClientPath string
  105. webAdminSetupPath string
  106. webLoginPath string
  107. webLogoutPath string
  108. webUsersPath string
  109. webUserPath string
  110. webConnectionsPath string
  111. webFoldersPath string
  112. webFolderPath string
  113. webStatusPath string
  114. webAdminsPath string
  115. webAdminPath string
  116. webMaintenancePath string
  117. webBackupPath string
  118. webRestorePath string
  119. webScanVFolderPath string
  120. webQuotaScanPath string
  121. webChangeAdminPwdPath string
  122. webTemplateUser string
  123. webTemplateFolder string
  124. webClientLoginPath string
  125. webClientFilesPath string
  126. webClientDirContentsPath string
  127. webClientDownloadZipPath string
  128. webClientCredentialsPath string
  129. webChangeClientPwdPath string
  130. webChangeClientKeysPath string
  131. webClientLogoutPath string
  132. webStaticFilesPath string
  133. )
  134. func init() {
  135. updateWebAdminURLs("")
  136. updateWebClientURLs("")
  137. }
  138. // Binding defines the configuration for a network listener
  139. type Binding struct {
  140. // The address to listen on. A blank value means listen on all available network interfaces.
  141. Address string `json:"address" mapstructure:"address"`
  142. // The port used for serving requests
  143. Port int `json:"port" mapstructure:"port"`
  144. // Enable the built-in admin interface.
  145. // You have to define TemplatesPath and StaticFilesPath for this to work
  146. EnableWebAdmin bool `json:"enable_web_admin" mapstructure:"enable_web_admin"`
  147. // Enable the built-in client interface.
  148. // You have to define TemplatesPath and StaticFilesPath for this to work
  149. EnableWebClient bool `json:"enable_web_client" mapstructure:"enable_web_client"`
  150. // you also need to provide a certificate for enabling HTTPS
  151. EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
  152. // set to 1 to require client certificate authentication in addition to basic auth.
  153. // You need to define at least a certificate authority for this to work
  154. ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
  155. // TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.
  156. // If CipherSuites is nil/empty, a default list of secure cipher suites
  157. // is used, with a preference order based on hardware performance.
  158. // Note that TLS 1.3 ciphersuites are not configurable.
  159. // The supported ciphersuites names are defined here:
  160. //
  161. // https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52
  162. //
  163. // any invalid name will be silently ignored.
  164. // The order matters, the ciphers listed first will be the preferred ones.
  165. TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
  166. // List of IP addresses and IP ranges allowed to set X-Forwarded-For, X-Real-IP,
  167. // X-Forwarded-Proto headers.
  168. ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
  169. allowHeadersFrom []func(net.IP) bool
  170. }
  171. func (b *Binding) parseAllowedProxy() error {
  172. allowedFuncs, err := utils.ParseAllowedIPAndRanges(b.ProxyAllowed)
  173. if err != nil {
  174. return err
  175. }
  176. b.allowHeadersFrom = allowedFuncs
  177. return nil
  178. }
  179. // GetAddress returns the binding address
  180. func (b *Binding) GetAddress() string {
  181. return fmt.Sprintf("%s:%d", b.Address, b.Port)
  182. }
  183. // IsValid returns true if the binding is valid
  184. func (b *Binding) IsValid() bool {
  185. if b.Port > 0 {
  186. return true
  187. }
  188. if filepath.IsAbs(b.Address) && runtime.GOOS != osWindows {
  189. return true
  190. }
  191. return false
  192. }
  193. type defenderStatus struct {
  194. IsActive bool `json:"is_active"`
  195. }
  196. // ServicesStatus keep the state of the running services
  197. type ServicesStatus struct {
  198. SSH sftpd.ServiceStatus `json:"ssh"`
  199. FTP ftpd.ServiceStatus `json:"ftp"`
  200. WebDAV webdavd.ServiceStatus `json:"webdav"`
  201. DataProvider dataprovider.ProviderStatus `json:"data_provider"`
  202. Defender defenderStatus `json:"defender"`
  203. }
  204. // Conf httpd daemon configuration
  205. type Conf struct {
  206. // Addresses and ports to bind to
  207. Bindings []Binding `json:"bindings" mapstructure:"bindings"`
  208. // Deprecated: please use Bindings
  209. BindPort int `json:"bind_port" mapstructure:"bind_port"`
  210. // Deprecated: please use Bindings
  211. BindAddress string `json:"bind_address" mapstructure:"bind_address"`
  212. // Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
  213. TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
  214. // Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir.
  215. // If both TemplatesPath and StaticFilesPath are empty the built-in web interface will be disabled
  216. StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
  217. // Path to the backup directory. This can be an absolute path or a path relative to the config dir
  218. BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
  219. // Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will
  220. // be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored.
  221. WebRoot string `json:"web_root" mapstructure:"web_root"`
  222. // If files containing a certificate and matching private key for the server are provided the server will expect
  223. // HTTPS connections.
  224. // Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
  225. // "paramchange" request to the running service on Windows.
  226. CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
  227. CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
  228. // CACertificates defines the set of root certificate authorities to be used to verify client certificates.
  229. CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
  230. // CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check
  231. // if a client certificate has been revoked
  232. CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"`
  233. }
  234. type apiResponse struct {
  235. Error string `json:"error,omitempty"`
  236. Message string `json:"message"`
  237. }
  238. // ShouldBind returns true if there is at least a valid binding
  239. func (c *Conf) ShouldBind() bool {
  240. for _, binding := range c.Bindings {
  241. if binding.IsValid() {
  242. return true
  243. }
  244. }
  245. return false
  246. }
  247. func (c *Conf) isWebAdminEnabled() bool {
  248. for _, binding := range c.Bindings {
  249. if binding.EnableWebAdmin {
  250. return true
  251. }
  252. }
  253. return false
  254. }
  255. func (c *Conf) isWebClientEnabled() bool {
  256. for _, binding := range c.Bindings {
  257. if binding.EnableWebClient {
  258. return true
  259. }
  260. }
  261. return false
  262. }
  263. func (c *Conf) checkRequiredDirs(staticFilesPath, templatesPath string) error {
  264. if (c.isWebAdminEnabled() || c.isWebClientEnabled()) && (staticFilesPath == "" || templatesPath == "") {
  265. return fmt.Errorf("required directory is invalid, static file path: %#v template path: %#v",
  266. staticFilesPath, templatesPath)
  267. }
  268. return nil
  269. }
  270. // Initialize configures and starts the HTTP server
  271. func (c *Conf) Initialize(configDir string) error {
  272. logger.Debug(logSender, "", "initializing HTTP server with config %+v", c)
  273. backupsPath = getConfigPath(c.BackupsPath, configDir)
  274. staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
  275. templatesPath := getConfigPath(c.TemplatesPath, configDir)
  276. if backupsPath == "" {
  277. return fmt.Errorf("required directory is invalid, backup path %#v", backupsPath)
  278. }
  279. if err := c.checkRequiredDirs(staticFilesPath, templatesPath); err != nil {
  280. return err
  281. }
  282. certificateFile := getConfigPath(c.CertificateFile, configDir)
  283. certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
  284. if c.isWebAdminEnabled() {
  285. updateWebAdminURLs(c.WebRoot)
  286. loadAdminTemplates(templatesPath)
  287. } else {
  288. logger.Info(logSender, "", "built-in web admin interface disabled")
  289. }
  290. if c.isWebClientEnabled() {
  291. updateWebClientURLs(c.WebRoot)
  292. loadClientTemplates(templatesPath)
  293. } else {
  294. logger.Info(logSender, "", "built-in web client interface disabled")
  295. }
  296. if certificateFile != "" && certificateKeyFile != "" {
  297. mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
  298. if err != nil {
  299. return err
  300. }
  301. mgr.SetCACertificates(c.CACertificates)
  302. if err := mgr.LoadRootCAs(); err != nil {
  303. return err
  304. }
  305. mgr.SetCARevocationLists(c.CARevocationLists)
  306. if err := mgr.LoadCRLs(); err != nil {
  307. return err
  308. }
  309. certMgr = mgr
  310. }
  311. csrfTokenAuth = jwtauth.New(jwa.HS256.String(), utils.GenerateRandomBytes(32), nil)
  312. exitChannel := make(chan error, 1)
  313. for _, binding := range c.Bindings {
  314. if !binding.IsValid() {
  315. continue
  316. }
  317. if err := binding.parseAllowedProxy(); err != nil {
  318. return err
  319. }
  320. go func(b Binding) {
  321. server := newHttpdServer(b, staticFilesPath)
  322. exitChannel <- server.listenAndServe()
  323. }(binding)
  324. }
  325. startJWTTokensCleanupTicker(tokenDuration)
  326. return <-exitChannel
  327. }
  328. func isWebRequest(r *http.Request) bool {
  329. return strings.HasPrefix(r.RequestURI, webBasePath+"/")
  330. }
  331. func isWebClientRequest(r *http.Request) bool {
  332. return strings.HasPrefix(r.RequestURI, webBaseClientPath+"/")
  333. }
  334. // ReloadCertificateMgr reloads the certificate manager
  335. func ReloadCertificateMgr() error {
  336. if certMgr != nil {
  337. return certMgr.Reload()
  338. }
  339. return nil
  340. }
  341. func getConfigPath(name, configDir string) string {
  342. if !utils.IsFileInputValid(name) {
  343. return ""
  344. }
  345. if name != "" && !filepath.IsAbs(name) {
  346. return filepath.Join(configDir, name)
  347. }
  348. return name
  349. }
  350. func getServicesStatus() ServicesStatus {
  351. status := ServicesStatus{
  352. SSH: sftpd.GetStatus(),
  353. FTP: ftpd.GetStatus(),
  354. WebDAV: webdavd.GetStatus(),
  355. DataProvider: dataprovider.GetProviderStatus(),
  356. Defender: defenderStatus{
  357. IsActive: common.Config.DefenderConfig.Enabled,
  358. },
  359. }
  360. return status
  361. }
  362. func getURLParam(r *http.Request, key string) string {
  363. v := chi.URLParam(r, key)
  364. unescaped, err := url.PathUnescape(v)
  365. if err != nil {
  366. return v
  367. }
  368. return unescaped
  369. }
  370. func fileServer(r chi.Router, path string, root http.FileSystem) {
  371. if path != "/" && path[len(path)-1] != '/' {
  372. r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
  373. path += "/"
  374. }
  375. path += "*"
  376. r.Get(path, func(w http.ResponseWriter, r *http.Request) {
  377. rctx := chi.RouteContext(r.Context())
  378. pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
  379. fs := http.StripPrefix(pathPrefix, http.FileServer(root))
  380. fs.ServeHTTP(w, r)
  381. })
  382. }
  383. func updateWebClientURLs(baseURL string) {
  384. if !path.IsAbs(baseURL) {
  385. baseURL = "/"
  386. }
  387. webRootPath = path.Join(baseURL, webRootPathDefault)
  388. webBasePath = path.Join(baseURL, webBasePathDefault)
  389. webBaseClientPath = path.Join(baseURL, webBasePathClientDefault)
  390. webClientLoginPath = path.Join(baseURL, webClientLoginPathDefault)
  391. webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
  392. webClientDirContentsPath = path.Join(baseURL, webClientDirContentsPathDefault)
  393. webClientDownloadZipPath = path.Join(baseURL, webClientDownloadZipPathDefault)
  394. webClientCredentialsPath = path.Join(baseURL, webClientCredentialsPathDefault)
  395. webChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault)
  396. webChangeClientKeysPath = path.Join(baseURL, webChangeClientKeysPathDefault)
  397. webClientLogoutPath = path.Join(baseURL, webClientLogoutPathDefault)
  398. }
  399. func updateWebAdminURLs(baseURL string) {
  400. if !path.IsAbs(baseURL) {
  401. baseURL = "/"
  402. }
  403. webRootPath = path.Join(baseURL, webRootPathDefault)
  404. webBasePath = path.Join(baseURL, webBasePathDefault)
  405. webBaseAdminPath = path.Join(baseURL, webBasePathAdminDefault)
  406. webAdminSetupPath = path.Join(baseURL, webAdminSetupPathDefault)
  407. webLoginPath = path.Join(baseURL, webLoginPathDefault)
  408. webLogoutPath = path.Join(baseURL, webLogoutPathDefault)
  409. webUsersPath = path.Join(baseURL, webUsersPathDefault)
  410. webUserPath = path.Join(baseURL, webUserPathDefault)
  411. webConnectionsPath = path.Join(baseURL, webConnectionsPathDefault)
  412. webFoldersPath = path.Join(baseURL, webFoldersPathDefault)
  413. webFolderPath = path.Join(baseURL, webFolderPathDefault)
  414. webStatusPath = path.Join(baseURL, webStatusPathDefault)
  415. webAdminsPath = path.Join(baseURL, webAdminsPathDefault)
  416. webAdminPath = path.Join(baseURL, webAdminPathDefault)
  417. webMaintenancePath = path.Join(baseURL, webMaintenancePathDefault)
  418. webBackupPath = path.Join(baseURL, webBackupPathDefault)
  419. webRestorePath = path.Join(baseURL, webRestorePathDefault)
  420. webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
  421. webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
  422. webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
  423. webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
  424. webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
  425. webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
  426. }
  427. // GetHTTPRouter returns an HTTP handler suitable to use for test cases
  428. func GetHTTPRouter() http.Handler {
  429. b := Binding{
  430. Address: "",
  431. Port: 8080,
  432. EnableWebAdmin: true,
  433. EnableWebClient: true,
  434. }
  435. server := newHttpdServer(b, "../static")
  436. server.initializeRouter()
  437. return server.router
  438. }
  439. // the ticker cannot be started/stopped from multiple goroutines
  440. func startJWTTokensCleanupTicker(duration time.Duration) {
  441. stopJWTTokensCleanupTicker()
  442. jwtTokensCleanupTicker = time.NewTicker(duration)
  443. jwtTokensCleanupDone = make(chan bool)
  444. go func() {
  445. for {
  446. select {
  447. case <-jwtTokensCleanupDone:
  448. return
  449. case <-jwtTokensCleanupTicker.C:
  450. cleanupExpiredJWTTokens()
  451. }
  452. }
  453. }()
  454. }
  455. func stopJWTTokensCleanupTicker() {
  456. if jwtTokensCleanupTicker != nil {
  457. jwtTokensCleanupTicker.Stop()
  458. jwtTokensCleanupDone <- true
  459. jwtTokensCleanupTicker = nil
  460. }
  461. }
  462. func cleanupExpiredJWTTokens() {
  463. invalidatedJWTTokens.Range(func(key, value interface{}) bool {
  464. exp, ok := value.(time.Time)
  465. if !ok || exp.Before(time.Now().UTC()) {
  466. invalidatedJWTTokens.Delete(key)
  467. }
  468. return true
  469. })
  470. }