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
This commit is contained in:
Nicola Murino 2020-06-18 23:53:38 +02:00
parent e86089a9f3
commit b30614e9d8
5 changed files with 61 additions and 29 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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) {

View file

@ -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) {

View file

@ -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{