| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- // Package httpd implements REST API and Web interface for SFTPGo.
- // The OpenAPI 3 schema for the exposed API can be found inside the source tree:
- // https://github.com/drakkan/sftpgo/blob/main/httpd/schema/openapi.yaml
- // A basic Web interface to manage users and connections is provided too
- package httpd
- import (
- "fmt"
- "net/http"
- "net/url"
- "path/filepath"
- "runtime"
- "strings"
- "sync"
- "time"
- "github.com/go-chi/chi/v5"
- "github.com/go-chi/jwtauth/v5"
- "github.com/drakkan/sftpgo/common"
- "github.com/drakkan/sftpgo/dataprovider"
- "github.com/drakkan/sftpgo/ftpd"
- "github.com/drakkan/sftpgo/logger"
- "github.com/drakkan/sftpgo/sftpd"
- "github.com/drakkan/sftpgo/utils"
- "github.com/drakkan/sftpgo/webdavd"
- )
- const (
- logSender = "httpd"
- tokenPath = "/api/v2/token"
- logoutPath = "/api/v2/logout"
- activeConnectionsPath = "/api/v2/connections"
- quotaScanPath = "/api/v2/quota-scans"
- quotaScanVFolderPath = "/api/v2/folder-quota-scans"
- userPath = "/api/v2/users"
- versionPath = "/api/v2/version"
- folderPath = "/api/v2/folders"
- serverStatusPath = "/api/v2/status"
- dumpDataPath = "/api/v2/dumpdata"
- loadDataPath = "/api/v2/loaddata"
- updateUsedQuotaPath = "/api/v2/quota-update"
- updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
- defenderBanTime = "/api/v2/defender/bantime"
- defenderUnban = "/api/v2/defender/unban"
- defenderScore = "/api/v2/defender/score"
- adminPath = "/api/v2/admins"
- adminPwdPath = "/api/v2/changepwd/admin"
- healthzPath = "/healthz"
- webBasePath = "/web"
- webLoginPath = "/web/login"
- webLogoutPath = "/web/logout"
- webUsersPath = "/web/users"
- webUserPath = "/web/user"
- webConnectionsPath = "/web/connections"
- webFoldersPath = "/web/folders"
- webFolderPath = "/web/folder"
- webStatusPath = "/web/status"
- webAdminsPath = "/web/admins"
- webAdminPath = "/web/admin"
- webMaintenancePath = "/web/maintenance"
- webBackupPath = "/web/backup"
- webRestorePath = "/web/restore"
- webScanVFolderPath = "/web/folder-quota-scans"
- webQuotaScanPath = "/web/quota-scans"
- webChangeAdminPwdPath = "/web/changepwd/admin"
- webTemplateUser = "/web/template/user"
- webTemplateFolder = "/web/template/folder"
- webStaticFilesPath = "/static"
- // MaxRestoreSize defines the max size for the loaddata input file
- MaxRestoreSize = 10485760 // 10 MB
- maxRequestSize = 1048576 // 1MB
- osWindows = "windows"
- )
- var (
- backupsPath string
- certMgr *common.CertManager
- jwtTokensCleanupTicker *time.Ticker
- jwtTokensCleanupDone chan bool
- invalidatedJWTTokens sync.Map
- csrfTokenAuth *jwtauth.JWTAuth
- )
- // Binding defines the configuration for a network listener
- type Binding struct {
- // The address to listen on. A blank value means listen on all available network interfaces.
- Address string `json:"address" mapstructure:"address"`
- // The port used for serving requests
- Port int `json:"port" mapstructure:"port"`
- // Enable the built-in admin interface.
- // You have to define TemplatesPath and StaticFilesPath for this to work
- EnableWebAdmin bool `json:"enable_web_admin" mapstructure:"enable_web_admin"`
- // you also need to provide a certificate for enabling HTTPS
- EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
- // set to 1 to require client certificate authentication in addition to basic auth.
- // You need to define at least a certificate authority for this to work
- ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
- // TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.
- // If CipherSuites is nil/empty, a default list of secure cipher suites
- // is used, with a preference order based on hardware performance.
- // Note that TLS 1.3 ciphersuites are not configurable.
- // The supported ciphersuites names are defined here:
- //
- // https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52
- //
- // any invalid name will be silently ignored.
- // The order matters, the ciphers listed first will be the preferred ones.
- TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
- }
- // GetAddress returns the binding address
- func (b *Binding) GetAddress() string {
- return fmt.Sprintf("%s:%d", b.Address, b.Port)
- }
- // IsValid returns true if the binding is valid
- func (b *Binding) IsValid() bool {
- if b.Port > 0 {
- return true
- }
- if filepath.IsAbs(b.Address) && runtime.GOOS != osWindows {
- return true
- }
- return false
- }
- type defenderStatus struct {
- IsActive bool `json:"is_active"`
- }
- // ServicesStatus keep the state of the running services
- type ServicesStatus struct {
- SSH sftpd.ServiceStatus `json:"ssh"`
- FTP ftpd.ServiceStatus `json:"ftp"`
- WebDAV webdavd.ServiceStatus `json:"webdav"`
- DataProvider dataprovider.ProviderStatus `json:"data_provider"`
- Defender defenderStatus `json:"defender"`
- }
- // Conf httpd daemon configuration
- type Conf struct {
- // Addresses and ports to bind to
- Bindings []Binding `json:"bindings" mapstructure:"bindings"`
- // Deprecated: please use Bindings
- BindPort int `json:"bind_port" mapstructure:"bind_port"`
- // Deprecated: please use Bindings
- BindAddress string `json:"bind_address" mapstructure:"bind_address"`
- // Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
- TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
- // Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir.
- // If both TemplatesPath and StaticFilesPath are empty the built-in web interface will be disabled
- StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
- // Path to the backup directory. This can be an absolute path or a path relative to the config dir
- BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
- // If files containing a certificate and matching private key for the server are provided the server will expect
- // HTTPS connections.
- // Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
- // "paramchange" request to the running service on Windows.
- CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
- CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
- // CACertificates defines the set of root certificate authorities to be used to verify client certificates.
- CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
- // CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check
- // if a client certificate has been revoked
- CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"`
- }
- type apiResponse struct {
- Error string `json:"error,omitempty"`
- Message string `json:"message"`
- }
- // ShouldBind returns true if there is at least a valid binding
- func (c *Conf) ShouldBind() bool {
- for _, binding := range c.Bindings {
- if binding.IsValid() {
- return true
- }
- }
- return false
- }
- // Initialize configures and starts the HTTP server
- func (c *Conf) Initialize(configDir string) error {
- logger.Debug(logSender, "", "initializing HTTP server with config %+v", c)
- backupsPath = getConfigPath(c.BackupsPath, configDir)
- staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
- templatesPath := getConfigPath(c.TemplatesPath, configDir)
- enableWebAdmin := staticFilesPath != "" || templatesPath != ""
- if backupsPath == "" {
- return fmt.Errorf("required directory is invalid, backup path %#v", backupsPath)
- }
- if enableWebAdmin && (staticFilesPath == "" || templatesPath == "") {
- return fmt.Errorf("required directory is invalid, static file path: %#v template path: %#v",
- staticFilesPath, templatesPath)
- }
- certificateFile := getConfigPath(c.CertificateFile, configDir)
- certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
- if enableWebAdmin {
- loadTemplates(templatesPath)
- } else {
- logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it")
- }
- if certificateFile != "" && certificateKeyFile != "" {
- mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
- if err != nil {
- return err
- }
- mgr.SetCACertificates(c.CACertificates)
- if err := mgr.LoadRootCAs(); err != nil {
- return err
- }
- mgr.SetCARevocationLists(c.CARevocationLists)
- if err := mgr.LoadCRLs(); err != nil {
- return err
- }
- certMgr = mgr
- }
- csrfTokenAuth = jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
- exitChannel := make(chan error, 1)
- for _, binding := range c.Bindings {
- if !binding.IsValid() {
- continue
- }
- go func(b Binding) {
- server := newHttpdServer(b, staticFilesPath, enableWebAdmin)
- exitChannel <- server.listenAndServe()
- }(binding)
- }
- startJWTTokensCleanupTicker(tokenDuration)
- return <-exitChannel
- }
- func isWebAdminRequest(r *http.Request) bool {
- return strings.HasPrefix(r.RequestURI, webBasePath+"/")
- }
- // ReloadCertificateMgr reloads the certificate manager
- func ReloadCertificateMgr() error {
- if certMgr != nil {
- return certMgr.Reload()
- }
- return nil
- }
- func getConfigPath(name, configDir string) string {
- if !utils.IsFileInputValid(name) {
- return ""
- }
- if name != "" && !filepath.IsAbs(name) {
- return filepath.Join(configDir, name)
- }
- return name
- }
- func getServicesStatus() ServicesStatus {
- status := ServicesStatus{
- SSH: sftpd.GetStatus(),
- FTP: ftpd.GetStatus(),
- WebDAV: webdavd.GetStatus(),
- DataProvider: dataprovider.GetProviderStatus(),
- Defender: defenderStatus{
- IsActive: common.Config.DefenderConfig.Enabled,
- },
- }
- return status
- }
- func getURLParam(r *http.Request, key string) string {
- v := chi.URLParam(r, key)
- unescaped, err := url.PathUnescape(v)
- if err != nil {
- return v
- }
- return unescaped
- }
- func fileServer(r chi.Router, path string, root http.FileSystem) {
- if path != "/" && path[len(path)-1] != '/' {
- r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
- path += "/"
- }
- path += "*"
- r.Get(path, func(w http.ResponseWriter, r *http.Request) {
- rctx := chi.RouteContext(r.Context())
- pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
- fs := http.StripPrefix(pathPrefix, http.FileServer(root))
- fs.ServeHTTP(w, r)
- })
- }
- // GetHTTPRouter returns an HTTP handler suitable to use for test cases
- func GetHTTPRouter() http.Handler {
- b := Binding{
- Address: "",
- Port: 8080,
- EnableWebAdmin: true,
- }
- server := newHttpdServer(b, "../static", true)
- server.initializeRouter()
- return server.router
- }
- // the ticker cannot be started/stopped from multiple goroutines
- func startJWTTokensCleanupTicker(duration time.Duration) {
- stopJWTTokensCleanupTicker()
- jwtTokensCleanupTicker = time.NewTicker(duration)
- jwtTokensCleanupDone = make(chan bool)
- go func() {
- for {
- select {
- case <-jwtTokensCleanupDone:
- return
- case <-jwtTokensCleanupTicker.C:
- cleanupExpiredJWTTokens()
- }
- }
- }()
- }
- func stopJWTTokensCleanupTicker() {
- if jwtTokensCleanupTicker != nil {
- jwtTokensCleanupTicker.Stop()
- jwtTokensCleanupDone <- true
- jwtTokensCleanupTicker = nil
- }
- }
- func cleanupExpiredJWTTokens() {
- invalidatedJWTTokens.Range(func(key, value interface{}) bool {
- exp, ok := value.(time.Time)
- if !ok || exp.Before(time.Now().UTC()) {
- invalidatedJWTTokens.Delete(key)
- }
- return true
- })
- }
|