// 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/master/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" "github.com/go-chi/chi" "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" 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" webScanVFolderPath = "/web/folder-quota-scans" webQuotaScanPath = "/web/quota-scans" webChangeAdminPwdPath = "/web/changepwd/admin" 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 ) // 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"` } 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 { // The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080 BindPort int `json:"bind_port" mapstructure:"bind_port"` // The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1" 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"` } type apiResponse struct { Error string `json:"error,omitempty"` Message string `json:"message"` } // ShouldBind returns true if there service must be started func (c Conf) ShouldBind() bool { if c.BindPort > 0 { return true } if filepath.IsAbs(c.BindAddress) && runtime.GOOS != osWindows { return true } return false } // Initialize configures and starts the HTTP server func (c Conf) Initialize(configDir string) error { var err 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 != "" { certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender) if err != nil { return err } } server := newHttpdServer(c.BindAddress, c.BindPort, staticFilesPath, enableWebAdmin) return server.listenAndServe() } 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 { server := newHttpdServer("", 8080, "../static", true) server.initializeRouter() return server.router }