httpd.go 18 KB

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