From b30614e9d8db3b5fdfa7156e5e67dcfcf155470b Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 18 Jun 2020 23:53:38 +0200 Subject: [PATCH] httpd: make the built-in web interface optional The built-in web admin will be disabled if both "templates_path" and "static_files_path" are empty Fixes #131 --- docs/full-configuration.md | 2 +- httpd/httpd.go | 23 +++++++++++++++------- httpd/httpd_test.go | 18 ++++++++++++++--- httpd/router.go | 40 +++++++++++++++++++++----------------- metrics/metrics.go | 7 +++++++ 5 files changed, 61 insertions(+), 29 deletions(-) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 6601f5c6..25176711 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -102,7 +102,7 @@ The configuration file contains the following sections: - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080 - `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1" - `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir - - `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir + - `static_files_path`, string. 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 `templates_path` and `static_files_path` are empty the built-in web interface will be disabled - `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons - `auth_user_file`, string. Path to a file used to store usernames and passwords for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication, and the file format must conform to the one generated using the Apache `htpasswd` tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty, HTTP authentication is disabled. - `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir. diff --git a/httpd/httpd.go b/httpd/httpd.go index 1e0dd972..7ef86dbf 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -61,7 +61,8 @@ type Conf struct { 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 + // 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"` @@ -91,15 +92,19 @@ func SetDataProvider(provider dataprovider.Provider) { } // Initialize configures and starts the HTTP server -func (c Conf) Initialize(configDir string, profiler bool) error { +func (c Conf) Initialize(configDir string, enableProfiler bool) 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) - if len(backupsPath) == 0 || len(staticFilesPath) == 0 || len(templatesPath) == 0 { - return fmt.Errorf("Required directory is invalid, backup path %#v, static file path: %#v template path: %#v", - backupsPath, staticFilesPath, templatesPath) + enableWebAdmin := len(staticFilesPath) > 0 || len(templatesPath) > 0 + if len(backupsPath) == 0 { + return fmt.Errorf("Required directory is invalid, backup path %#v", backupsPath) + } + if enableWebAdmin && (len(staticFilesPath) == 0 || len(templatesPath) == 0) { + return fmt.Errorf("Required directory is invalid, static file path: %#v template path: %#v", + staticFilesPath, templatesPath) } authUserFile := getConfigPath(c.AuthUserFile, configDir) httpAuth, err = newBasicAuthProvider(authUserFile) @@ -108,8 +113,12 @@ func (c Conf) Initialize(configDir string, profiler bool) error { } certificateFile := getConfigPath(c.CertificateFile, configDir) certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir) - loadTemplates(templatesPath) - initializeRouter(staticFilesPath, profiler) + if enableWebAdmin { + loadTemplates(templatesPath) + } else { + logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it") + } + initializeRouter(staticFilesPath, enableProfiler, enableWebAdmin) httpServer := &http.Server{ Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort), Handler: router, diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 0627a48f..edadd309 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -178,15 +178,16 @@ func TestMain(m *testing.M) { func TestInitialization(t *testing.T) { err := config.LoadConfig(configDir, "") assert.NoError(t, err) + invalidFile := "invalid file" httpdConf := config.GetHTTPDConfig() httpdConf.BackupsPath = "test_backups" - httpdConf.AuthUserFile = "invalid_file" + httpdConf.AuthUserFile = invalidFile err = httpdConf.Initialize(configDir, true) assert.Error(t, err) httpdConf.BackupsPath = backupsPath httpdConf.AuthUserFile = "" - httpdConf.CertificateFile = "invalid file" - httpdConf.CertificateKeyFile = "invalid file" + httpdConf.CertificateFile = invalidFile + httpdConf.CertificateKeyFile = invalidFile err = httpdConf.Initialize(configDir, true) assert.Error(t, err) httpdConf.CertificateFile = "" @@ -196,6 +197,17 @@ func TestInitialization(t *testing.T) { assert.Error(t, err) err = httpd.ReloadTLSCertificate() assert.NoError(t, err, "reloading TLS Certificate must return nil error if no certificate is configured") + httpdConf = config.GetHTTPDConfig() + httpdConf.BackupsPath = ".." + err = httpdConf.Initialize(configDir, true) + assert.Error(t, err) + httpdConf.BackupsPath = backupsPath + httpdConf.CertificateFile = invalidFile + httpdConf.CertificateKeyFile = invalidFile + httpdConf.StaticFilesPath = "" + httpdConf.TemplatesPath = "" + err = httpdConf.Initialize(configDir, true) + assert.Error(t, err) } func TestBasicUserHandling(t *testing.T) { diff --git a/httpd/router.go b/httpd/router.go index 1cf999f3..068c7058 100644 --- a/httpd/router.go +++ b/httpd/router.go @@ -7,10 +7,10 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/go-chi/render" - "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/logger" + "github.com/drakkan/sftpgo/metrics" "github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/utils" ) @@ -20,14 +20,14 @@ func GetHTTPRouter() http.Handler { return router } -func initializeRouter(staticFilesPath string, profiler bool) { +func initializeRouter(staticFilesPath string, enableProfiler, enableWebAdmin bool) { router = chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.RealIP) router.Use(logger.NewStructuredLogger(logger.GetLogger())) router.Use(middleware.Recoverer) - if profiler { + if enableProfiler { logger.InfoToConsole("enabling the built-in profiler") logger.Info(logSender, "", "enabling the built-in profiler") router.Mount(pprofBasePath, middleware.Profiler()) @@ -52,7 +52,7 @@ func initializeRouter(staticFilesPath string, profiler bool) { http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently) }) - router.Handle(metricsPath, promhttp.Handler()) + metrics.AddMetricsEndpoint(metricsPath, router) router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, utils.GetAppVersion()) @@ -86,22 +86,26 @@ func initializeRouter(staticFilesPath string, profiler bool) { router.Delete(folderPath, deleteFolderByPath) router.Get(dumpDataPath, dumpData) router.Get(loadDataPath, loadData) - router.Get(webUsersPath, handleGetWebUsers) - router.Get(webUserPath, handleWebAddUserGet) - router.Get(webUserPath+"/{userID}", handleWebUpdateUserGet) - router.Post(webUserPath, handleWebAddUserPost) - router.Post(webUserPath+"/{userID}", handleWebUpdateUserPost) - router.Get(webConnectionsPath, handleWebGetConnections) - router.Get(webFoldersPath, handleWebGetFolders) - router.Get(webFolderPath, handleWebAddFolderGet) - router.Post(webFolderPath, handleWebAddFolderPost) + if enableWebAdmin { + router.Get(webUsersPath, handleGetWebUsers) + router.Get(webUserPath, handleWebAddUserGet) + router.Get(webUserPath+"/{userID}", handleWebUpdateUserGet) + router.Post(webUserPath, handleWebAddUserPost) + router.Post(webUserPath+"/{userID}", handleWebUpdateUserPost) + router.Get(webConnectionsPath, handleWebGetConnections) + router.Get(webFoldersPath, handleWebGetFolders) + router.Get(webFolderPath, handleWebAddFolderGet) + router.Post(webFolderPath, handleWebAddFolderPost) + } }) - router.Group(func(router chi.Router) { - compressor := middleware.NewCompressor(5) - router.Use(compressor.Handler) - fileServer(router, webStaticFilesPath, http.Dir(staticFilesPath)) - }) + if enableWebAdmin { + router.Group(func(router chi.Router) { + compressor := middleware.NewCompressor(5) + router.Use(compressor.Handler) + fileServer(router, webStaticFilesPath, http.Dir(staticFilesPath)) + }) + } } func handleCloseConnection(w http.ResponseWriter, r *http.Request) { diff --git a/metrics/metrics.go b/metrics/metrics.go index 1321e893..ffcc2f74 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -2,8 +2,10 @@ package metrics import ( + "github.com/go-chi/chi" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" ) const ( @@ -13,6 +15,11 @@ const ( loginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive" ) +// AddMetricsEndpoint exposes metrics to the specified endpoint +func AddMetricsEndpoint(metricsPath string, handler chi.Router) { + handler.Handle(metricsPath, promhttp.Handler()) +} + var ( // dataproviderAvailability is the metric that reports the availability for the configured data provider dataproviderAvailability = promauto.NewGauge(prometheus.GaugeOpts{