diff --git a/config/config.go b/config/config.go
index 2c37b218..9cc1c6c5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -218,8 +218,11 @@ func Init() {
TemplatesPath: "templates",
StaticFilesPath: "static",
BackupsPath: "backups",
+ WebAdminRoot: "",
CertificateFile: "",
CertificateKeyFile: "",
+ CACertificates: nil,
+ CARevocationLists: nil,
},
HTTPConfig: httpclient.Config{
Timeout: 20,
@@ -857,6 +860,7 @@ func setViperDefaults() {
viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath)
+ viper.SetDefault("httpd.web_admin_root", globalConf.HTTPDConfig.WebAdminRoot)
viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile)
viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)
viper.SetDefault("httpd.ca_certificates", globalConf.HTTPDConfig.CACertificates)
diff --git a/docs/full-configuration.md b/docs/full-configuration.md
index ffc76694..75ef9797 100644
--- a/docs/full-configuration.md
+++ b/docs/full-configuration.md
@@ -204,6 +204,7 @@ The configuration file contains the following sections:
- `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. 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
+ - `web_admin_root`, string. Defines a base URL for the web admin. If empty web admin resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key 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.
- `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
diff --git a/httpd/httpd.go b/httpd/httpd.go
index b6ebf7a6..4471397c 100644
--- a/httpd/httpd.go
+++ b/httpd/httpd.go
@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "path"
"path/filepath"
"runtime"
"strings"
@@ -27,46 +28,47 @@ import (
)
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"
+ 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"
+ webRootPathDefault = "/"
+ webBasePathDefault = "/web"
+ webLoginPathDefault = "/web/login"
+ webLogoutPathDefault = "/web/logout"
+ webUsersPathDefault = "/web/users"
+ webUserPathDefault = "/web/user"
+ webConnectionsPathDefault = "/web/connections"
+ webFoldersPathDefault = "/web/folders"
+ webFolderPathDefault = "/web/folder"
+ webStatusPathDefault = "/web/status"
+ webAdminsPathDefault = "/web/admins"
+ webAdminPathDefault = "/web/admin"
+ webMaintenancePathDefault = "/web/maintenance"
+ webBackupPathDefault = "/web/backup"
+ webRestorePathDefault = "/web/restore"
+ webScanVFolderPathDefault = "/web/folder-quota-scans"
+ webQuotaScanPathDefault = "/web/quota-scans"
+ webChangeAdminPwdPathDefault = "/web/changepwd/admin"
+ webTemplateUserDefault = "/web/template/user"
+ webTemplateFolderDefault = "/web/template/folder"
+ webStaticFilesPathDefault = "/static"
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB
maxRequestSize = 1048576 // 1MB
@@ -80,8 +82,33 @@ var (
jwtTokensCleanupDone chan bool
invalidatedJWTTokens sync.Map
csrfTokenAuth *jwtauth.JWTAuth
+ webRootPath string
+ webBasePath string
+ webLoginPath string
+ webLogoutPath string
+ webUsersPath string
+ webUserPath string
+ webConnectionsPath string
+ webFoldersPath string
+ webFolderPath string
+ webStatusPath string
+ webAdminsPath string
+ webAdminPath string
+ webMaintenancePath string
+ webBackupPath string
+ webRestorePath string
+ webScanVFolderPath string
+ webQuotaScanPath string
+ webChangeAdminPwdPath string
+ webTemplateUser string
+ webTemplateFolder string
+ webStaticFilesPath string
)
+func init() {
+ updateWebAdminURLs("")
+}
+
// 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.
@@ -153,6 +180,9 @@ type Conf struct {
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"`
+ // Defines a base URL for the web admin. If empty web admin resources will be available at the
+ // root ("/") URI. If defined it must be an absolute URI or it will be ignored.
+ WebAdminRoot string `json:"web_admin_root" mapstructure:"web_admin_root"`
// 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
@@ -199,6 +229,7 @@ func (c *Conf) Initialize(configDir string) error {
certificateFile := getConfigPath(c.CertificateFile, configDir)
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
if enableWebAdmin {
+ updateWebAdminURLs(c.WebAdminRoot)
loadTemplates(templatesPath)
} else {
logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it")
@@ -298,6 +329,33 @@ func fileServer(r chi.Router, path string, root http.FileSystem) {
})
}
+func updateWebAdminURLs(baseURL string) {
+ if !path.IsAbs(baseURL) {
+ baseURL = "/"
+ }
+ webRootPath = path.Join(baseURL, webRootPathDefault)
+ webBasePath = path.Join(baseURL, webBasePathDefault)
+ webLoginPath = path.Join(baseURL, webLoginPathDefault)
+ webLogoutPath = path.Join(baseURL, webLogoutPathDefault)
+ webUsersPath = path.Join(baseURL, webUsersPathDefault)
+ webUserPath = path.Join(baseURL, webUserPathDefault)
+ webConnectionsPath = path.Join(baseURL, webConnectionsPathDefault)
+ webFoldersPath = path.Join(baseURL, webFoldersPathDefault)
+ webFolderPath = path.Join(baseURL, webFolderPathDefault)
+ webStatusPath = path.Join(baseURL, webStatusPathDefault)
+ webAdminsPath = path.Join(baseURL, webAdminsPathDefault)
+ webAdminPath = path.Join(baseURL, webAdminPathDefault)
+ webMaintenancePath = path.Join(baseURL, webMaintenancePathDefault)
+ webBackupPath = path.Join(baseURL, webBackupPathDefault)
+ webRestorePath = path.Join(baseURL, webRestorePathDefault)
+ webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
+ webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
+ webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
+ webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
+ webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
+ webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
+}
+
// GetHTTPRouter returns an HTTP handler suitable to use for test cases
func GetHTTPRouter() http.Handler {
b := Binding{
diff --git a/httpd/server.go b/httpd/server.go
index d05abfa9..f4ddbb11 100644
--- a/httpd/server.go
+++ b/httpd/server.go
@@ -257,6 +257,7 @@ func (s *httpdServer) initializeRouter() {
s.router.Use(saveConnectionAddress)
s.router.Use(middleware.GetHead)
+ s.router.Use(middleware.StripSlashes)
s.router.Group(func(r chi.Router) {
r.Get(healthzPath, func(w http.ResponseWriter, r *http.Request) {
@@ -334,7 +335,7 @@ func (s *httpdServer) initializeRouter() {
})
if s.enableWebAdmin {
- router.Get("/", func(w http.ResponseWriter, r *http.Request) {
+ router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webLoginPath, http.StatusMovedPermanently)
})
diff --git a/httpd/web.go b/httpd/web.go
index 88eb3a1f..5e4c87f6 100644
--- a/httpd/web.go
+++ b/httpd/web.go
@@ -96,6 +96,7 @@ type basePage struct {
FolderQuotaScanURL string
StatusURL string
MaintenanceURL string
+ StaticURL string
UsersTitle string
AdminsTitle string
ConnectionsTitle string
@@ -182,6 +183,7 @@ type loginPage struct {
Version string
Error string
CSRFToken string
+ StaticURL string
}
type userTemplateFields struct {
@@ -290,6 +292,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
StatusURL: webStatusPath,
FolderQuotaScanURL: webScanVFolderPath,
MaintenanceURL: webMaintenancePath,
+ StaticURL: webStaticFilesPath,
UsersTitle: pageUsersTitle,
AdminsTitle: pageAdminsTitle,
ConnectionsTitle: pageConnectionsTitle,
@@ -1030,6 +1033,7 @@ func renderLoginPage(w http.ResponseWriter, error string) {
Version: version.Get().Version,
Error: error,
CSRFToken: createCSRFToken(),
+ StaticURL: webStaticFilesPath,
}
renderTemplate(w, templateLogin, data)
}
diff --git a/sftpgo.json b/sftpgo.json
index b1c870de..05903b8f 100644
--- a/sftpgo.json
+++ b/sftpgo.json
@@ -165,6 +165,7 @@
"templates_path": "templates",
"static_files_path": "static",
"backups_path": "backups",
+ "web_admin_root": "",
"certificate_file": "",
"certificate_key_file": "",
"ca_certificates": [],
diff --git a/static/css/fonts.css b/static/css/fonts.css
deleted file mode 100644
index fe4c8833..00000000
--- a/static/css/fonts.css
+++ /dev/null
@@ -1,20 +0,0 @@
-@font-face {
- font-family: 'Roboto';
- src: url('/static/vendor/fonts/Roboto-Bold-webfont.woff');
- font-weight: 700;
- font-style: normal;
- }
-
- @font-face {
- font-family: 'Roboto';
- src: url('/static/vendor/fonts/Roboto-Regular-webfont.woff');
- font-weight: 400;
- font-style: normal;
- }
-
- @font-face {
- font-family: 'Roboto';
- src: url('/static/vendor/fonts/Roboto-Light-webfont.woff');
- font-weight: 300;
- font-style: normal;
- }
\ No newline at end of file
diff --git a/templates/admins.html b/templates/admins.html
index ef7c29c3..f4821e07 100644
--- a/templates/admins.html
+++ b/templates/admins.html
@@ -3,11 +3,11 @@
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
-
-
-
-
-
+
+
+
+
+
{{end}}
{{define "page_body"}}
@@ -83,15 +83,15 @@
{{end}}
{{define "extra_js"}}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
-
+
-
+