1
0

httpd.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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. // A basic Web interface to manage users and connections is provided too
  5. package httpd
  6. import (
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "path/filepath"
  11. "runtime"
  12. "strings"
  13. "sync"
  14. "time"
  15. "github.com/go-chi/chi/v5"
  16. "github.com/go-chi/jwtauth/v5"
  17. "github.com/drakkan/sftpgo/common"
  18. "github.com/drakkan/sftpgo/dataprovider"
  19. "github.com/drakkan/sftpgo/ftpd"
  20. "github.com/drakkan/sftpgo/logger"
  21. "github.com/drakkan/sftpgo/sftpd"
  22. "github.com/drakkan/sftpgo/utils"
  23. "github.com/drakkan/sftpgo/webdavd"
  24. )
  25. const (
  26. logSender = "httpd"
  27. tokenPath = "/api/v2/token"
  28. logoutPath = "/api/v2/logout"
  29. activeConnectionsPath = "/api/v2/connections"
  30. quotaScanPath = "/api/v2/quota-scans"
  31. quotaScanVFolderPath = "/api/v2/folder-quota-scans"
  32. userPath = "/api/v2/users"
  33. versionPath = "/api/v2/version"
  34. folderPath = "/api/v2/folders"
  35. serverStatusPath = "/api/v2/status"
  36. dumpDataPath = "/api/v2/dumpdata"
  37. loadDataPath = "/api/v2/loaddata"
  38. updateUsedQuotaPath = "/api/v2/quota-update"
  39. updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
  40. defenderBanTime = "/api/v2/defender/bantime"
  41. defenderUnban = "/api/v2/defender/unban"
  42. defenderScore = "/api/v2/defender/score"
  43. adminPath = "/api/v2/admins"
  44. adminPwdPath = "/api/v2/changepwd/admin"
  45. healthzPath = "/healthz"
  46. webBasePath = "/web"
  47. webLoginPath = "/web/login"
  48. webLogoutPath = "/web/logout"
  49. webUsersPath = "/web/users"
  50. webUserPath = "/web/user"
  51. webConnectionsPath = "/web/connections"
  52. webFoldersPath = "/web/folders"
  53. webFolderPath = "/web/folder"
  54. webStatusPath = "/web/status"
  55. webAdminsPath = "/web/admins"
  56. webAdminPath = "/web/admin"
  57. webMaintenancePath = "/web/maintenance"
  58. webBackupPath = "/web/backup"
  59. webRestorePath = "/web/restore"
  60. webScanVFolderPath = "/web/folder-quota-scans"
  61. webQuotaScanPath = "/web/quota-scans"
  62. webChangeAdminPwdPath = "/web/changepwd/admin"
  63. webTemplateUser = "/web/template/user"
  64. webTemplateFolder = "/web/template/folder"
  65. webStaticFilesPath = "/static"
  66. // MaxRestoreSize defines the max size for the loaddata input file
  67. MaxRestoreSize = 10485760 // 10 MB
  68. maxRequestSize = 1048576 // 1MB
  69. osWindows = "windows"
  70. )
  71. var (
  72. backupsPath string
  73. certMgr *common.CertManager
  74. jwtTokensCleanupTicker *time.Ticker
  75. jwtTokensCleanupDone chan bool
  76. invalidatedJWTTokens sync.Map
  77. csrfTokenAuth *jwtauth.JWTAuth
  78. )
  79. // Binding defines the configuration for a network listener
  80. type Binding struct {
  81. // The address to listen on. A blank value means listen on all available network interfaces.
  82. Address string `json:"address" mapstructure:"address"`
  83. // The port used for serving requests
  84. Port int `json:"port" mapstructure:"port"`
  85. // Enable the built-in admin interface.
  86. // You have to define TemplatesPath and StaticFilesPath for this to work
  87. EnableWebAdmin bool `json:"enable_web_admin" mapstructure:"enable_web_admin"`
  88. // you also need to provide a certificate for enabling HTTPS
  89. EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
  90. // set to 1 to require client certificate authentication in addition to basic auth.
  91. // You need to define at least a certificate authority for this to work
  92. ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
  93. // TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.
  94. // If CipherSuites is nil/empty, a default list of secure cipher suites
  95. // is used, with a preference order based on hardware performance.
  96. // Note that TLS 1.3 ciphersuites are not configurable.
  97. // The supported ciphersuites names are defined here:
  98. //
  99. // https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52
  100. //
  101. // any invalid name will be silently ignored.
  102. // The order matters, the ciphers listed first will be the preferred ones.
  103. TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
  104. }
  105. // GetAddress returns the binding address
  106. func (b *Binding) GetAddress() string {
  107. return fmt.Sprintf("%s:%d", b.Address, b.Port)
  108. }
  109. // IsValid returns true if the binding is valid
  110. func (b *Binding) IsValid() bool {
  111. if b.Port > 0 {
  112. return true
  113. }
  114. if filepath.IsAbs(b.Address) && runtime.GOOS != osWindows {
  115. return true
  116. }
  117. return false
  118. }
  119. type defenderStatus struct {
  120. IsActive bool `json:"is_active"`
  121. }
  122. // ServicesStatus keep the state of the running services
  123. type ServicesStatus struct {
  124. SSH sftpd.ServiceStatus `json:"ssh"`
  125. FTP ftpd.ServiceStatus `json:"ftp"`
  126. WebDAV webdavd.ServiceStatus `json:"webdav"`
  127. DataProvider dataprovider.ProviderStatus `json:"data_provider"`
  128. Defender defenderStatus `json:"defender"`
  129. }
  130. // Conf httpd daemon configuration
  131. type Conf struct {
  132. // Addresses and ports to bind to
  133. Bindings []Binding `json:"bindings" mapstructure:"bindings"`
  134. // Deprecated: please use Bindings
  135. BindPort int `json:"bind_port" mapstructure:"bind_port"`
  136. // Deprecated: please use Bindings
  137. BindAddress string `json:"bind_address" mapstructure:"bind_address"`
  138. // Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
  139. TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
  140. // Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir.
  141. // If both TemplatesPath and StaticFilesPath are empty the built-in web interface will be disabled
  142. StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
  143. // Path to the backup directory. This can be an absolute path or a path relative to the config dir
  144. BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
  145. // If files containing a certificate and matching private key for the server are provided the server will expect
  146. // HTTPS connections.
  147. // Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
  148. // "paramchange" request to the running service on Windows.
  149. CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
  150. CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
  151. // CACertificates defines the set of root certificate authorities to be used to verify client certificates.
  152. CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
  153. // CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check
  154. // if a client certificate has been revoked
  155. CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"`
  156. }
  157. type apiResponse struct {
  158. Error string `json:"error,omitempty"`
  159. Message string `json:"message"`
  160. }
  161. // ShouldBind returns true if there is at least a valid binding
  162. func (c *Conf) ShouldBind() bool {
  163. for _, binding := range c.Bindings {
  164. if binding.IsValid() {
  165. return true
  166. }
  167. }
  168. return false
  169. }
  170. // Initialize configures and starts the HTTP server
  171. func (c *Conf) Initialize(configDir string) error {
  172. logger.Debug(logSender, "", "initializing HTTP server with config %+v", c)
  173. backupsPath = getConfigPath(c.BackupsPath, configDir)
  174. staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
  175. templatesPath := getConfigPath(c.TemplatesPath, configDir)
  176. enableWebAdmin := staticFilesPath != "" || templatesPath != ""
  177. if backupsPath == "" {
  178. return fmt.Errorf("required directory is invalid, backup path %#v", backupsPath)
  179. }
  180. if enableWebAdmin && (staticFilesPath == "" || templatesPath == "") {
  181. return fmt.Errorf("required directory is invalid, static file path: %#v template path: %#v",
  182. staticFilesPath, templatesPath)
  183. }
  184. certificateFile := getConfigPath(c.CertificateFile, configDir)
  185. certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
  186. if enableWebAdmin {
  187. loadTemplates(templatesPath)
  188. } else {
  189. logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it")
  190. }
  191. if certificateFile != "" && certificateKeyFile != "" {
  192. mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
  193. if err != nil {
  194. return err
  195. }
  196. mgr.SetCACertificates(c.CACertificates)
  197. if err := mgr.LoadRootCAs(); err != nil {
  198. return err
  199. }
  200. mgr.SetCARevocationLists(c.CARevocationLists)
  201. if err := mgr.LoadCRLs(); err != nil {
  202. return err
  203. }
  204. certMgr = mgr
  205. }
  206. csrfTokenAuth = jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
  207. exitChannel := make(chan error, 1)
  208. for _, binding := range c.Bindings {
  209. if !binding.IsValid() {
  210. continue
  211. }
  212. go func(b Binding) {
  213. server := newHttpdServer(b, staticFilesPath, enableWebAdmin)
  214. exitChannel <- server.listenAndServe()
  215. }(binding)
  216. }
  217. startJWTTokensCleanupTicker(tokenDuration)
  218. return <-exitChannel
  219. }
  220. func isWebAdminRequest(r *http.Request) bool {
  221. return strings.HasPrefix(r.RequestURI, webBasePath+"/")
  222. }
  223. // ReloadCertificateMgr reloads the certificate manager
  224. func ReloadCertificateMgr() error {
  225. if certMgr != nil {
  226. return certMgr.Reload()
  227. }
  228. return nil
  229. }
  230. func getConfigPath(name, configDir string) string {
  231. if !utils.IsFileInputValid(name) {
  232. return ""
  233. }
  234. if name != "" && !filepath.IsAbs(name) {
  235. return filepath.Join(configDir, name)
  236. }
  237. return name
  238. }
  239. func getServicesStatus() ServicesStatus {
  240. status := ServicesStatus{
  241. SSH: sftpd.GetStatus(),
  242. FTP: ftpd.GetStatus(),
  243. WebDAV: webdavd.GetStatus(),
  244. DataProvider: dataprovider.GetProviderStatus(),
  245. Defender: defenderStatus{
  246. IsActive: common.Config.DefenderConfig.Enabled,
  247. },
  248. }
  249. return status
  250. }
  251. func getURLParam(r *http.Request, key string) string {
  252. v := chi.URLParam(r, key)
  253. unescaped, err := url.PathUnescape(v)
  254. if err != nil {
  255. return v
  256. }
  257. return unescaped
  258. }
  259. func fileServer(r chi.Router, path string, root http.FileSystem) {
  260. if path != "/" && path[len(path)-1] != '/' {
  261. r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
  262. path += "/"
  263. }
  264. path += "*"
  265. r.Get(path, func(w http.ResponseWriter, r *http.Request) {
  266. rctx := chi.RouteContext(r.Context())
  267. pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
  268. fs := http.StripPrefix(pathPrefix, http.FileServer(root))
  269. fs.ServeHTTP(w, r)
  270. })
  271. }
  272. // GetHTTPRouter returns an HTTP handler suitable to use for test cases
  273. func GetHTTPRouter() http.Handler {
  274. b := Binding{
  275. Address: "",
  276. Port: 8080,
  277. EnableWebAdmin: true,
  278. }
  279. server := newHttpdServer(b, "../static", true)
  280. server.initializeRouter()
  281. return server.router
  282. }
  283. // the ticker cannot be started/stopped from multiple goroutines
  284. func startJWTTokensCleanupTicker(duration time.Duration) {
  285. stopJWTTokensCleanupTicker()
  286. jwtTokensCleanupTicker = time.NewTicker(duration)
  287. jwtTokensCleanupDone = make(chan bool)
  288. go func() {
  289. for {
  290. select {
  291. case <-jwtTokensCleanupDone:
  292. return
  293. case <-jwtTokensCleanupTicker.C:
  294. cleanupExpiredJWTTokens()
  295. }
  296. }
  297. }()
  298. }
  299. func stopJWTTokensCleanupTicker() {
  300. if jwtTokensCleanupTicker != nil {
  301. jwtTokensCleanupTicker.Stop()
  302. jwtTokensCleanupDone <- true
  303. jwtTokensCleanupTicker = nil
  304. }
  305. }
  306. func cleanupExpiredJWTTokens() {
  307. invalidatedJWTTokens.Range(func(key, value interface{}) bool {
  308. exp, ok := value.(time.Time)
  309. if !ok || exp.Before(time.Now().UTC()) {
  310. invalidatedJWTTokens.Delete(key)
  311. }
  312. return true
  313. })
  314. }