diff --git a/config/config.go b/config/config.go index 69090065..f06ce49c 100644 --- a/config/config.go +++ b/config/config.go @@ -98,6 +98,8 @@ var ( AllowedHosts: nil, AllowedHostsAreRegex: false, HostsProxyHeaders: nil, + HTTPSRedirect: false, + HTTPSHost: "", HTTPSProxyHeaders: nil, STSSeconds: 0, STSIncludeSubdomains: false, @@ -1142,7 +1144,7 @@ func getHTTPDSecurityProxyHeadersFromEnv(idx int) []httpd.HTTPSProxyHeader { return httpsProxyHeaders } -func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { +func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:gocyclo var result httpd.SecurityConf isSet := false @@ -1170,6 +1172,18 @@ func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { isSet = true } + httpsRedirect, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_REDIRECT", idx)) + if ok { + result.HTTPSRedirect = httpsRedirect + isSet = true + } + + httpsHost, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_HOST", idx)) + if ok { + result.HTTPSHost = httpsHost + isSet = true + } + httpsProxyHeaders := getHTTPDSecurityProxyHeadersFromEnv(idx) if len(httpsProxyHeaders) > 0 { result.HTTPSProxyHeaders = httpsProxyHeaders diff --git a/config/config_test.go b/config/config_test.go index 7f60d715..633529c4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -854,6 +854,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS", "*.example.com,*.example.net") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX", "1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HOSTS_PROXY_HEADERS", "X-Forwarded-Host") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_REDIRECT", "1") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_HOST", "www.example.com") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__KEY", "X-Forwarded-Proto") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__VALUE", "https") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_SECONDS", "31536000") @@ -900,6 +902,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HOSTS_PROXY_HEADERS") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_REDIRECT") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_HOST") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__KEY") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__VALUE") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_SECONDS") @@ -975,6 +979,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.True(t, bindings[2].Security.AllowedHostsAreRegex) require.Len(t, bindings[2].Security.HostsProxyHeaders, 1) require.Equal(t, "X-Forwarded-Host", bindings[2].Security.HostsProxyHeaders[0]) + require.True(t, bindings[2].Security.HTTPSRedirect) + require.Equal(t, "www.example.com", bindings[2].Security.HTTPSHost) require.Len(t, bindings[2].Security.HTTPSProxyHeaders, 1) require.Equal(t, "X-Forwarded-Proto", bindings[2].Security.HTTPSProxyHeaders[0].Key) require.Equal(t, "https", bindings[2].Security.HTTPSProxyHeaders[0].Value) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 6d7a0f4c..59459e95 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -256,6 +256,8 @@ The configuration file contains the following sections: - `allowed_hosts`, list of strings. Fully qualified domain names that are allowed. An empty list allows any and all host names. Default: empty. - `allowed_hosts_are_regex`, boolean. Determines if the provided allowed hosts contains valid regular expressions. Default: `false`. - `hosts_proxy_headers`, list of string. Defines a set of header keys that may hold a proxied hostname value for the request, for example `X-Forwarded-Host`. Default: empty. + - `https_redirect`, boolean. Set to `true` to redirect HTTP requests to HTTPS. Default: `false`. + - `https_host`, string. Defines the host name that is used to redirect HTTP requests to HTTPS. Default is blank, which indicates to use the same host. For example, if `https_redirect` is enabled and `https_host` is blank, a request for `http://127.0.0.1/web/client/login` will be redirected to `https://127.0.0.1/web/client/login`, if `https_host` is set to `www.example.com` the same request will be redirected to `https://www.example.com/web/client/login`. - `https_proxy_headers`, list of struct, each struct contains the fields `key` and `value`. Defines a a list of header keys with associated values that would indicate a valid https request. For example `key` could be `X-Forwarded-Proto` and `value` `https`. Default: empty. - `sts_seconds`, integer. Defines the max-age of the `Strict-Transport-Security` header. This header will be included for `https` responses or for HTTP request if the request includes a defined HTTPS proxy header. Default: `0`, which would NOT include the header. - `sts_include_subdomains`, boolean. Set to `true`, the `includeSubdomains` will be appended to the `Strict-Transport-Security` header. Default: `false`. diff --git a/httpd/httpd.go b/httpd/httpd.go index 1df3f809..93eeaae4 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -255,6 +255,11 @@ type SecurityConf struct { AllowedHostsAreRegex bool `json:"allowed_hosts_are_regex" mapstructure:"allowed_hosts_are_regex"` // HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request. HostsProxyHeaders []string `json:"hosts_proxy_headers" mapstructure:"hosts_proxy_headers"` + // Set to true to redirect HTTP requests to HTTPS + HTTPSRedirect bool `json:"https_redirect" mapstructure:"https_redirect"` + // HTTPSHost defines the host name that is used to redirect HTTP requests to HTTPS. + // Default is "", which indicates to use the same host. + HTTPSHost string `json:"https_host" mapstructure:"https_host"` // HTTPSProxyHeaders is a list of header keys with associated values that would indicate a valid https request. HTTPSProxyHeaders []HTTPSProxyHeader `json:"https_proxy_headers" mapstructure:"https_proxy_headers"` // STSSeconds is the max-age of the Strict-Transport-Security header. diff --git a/httpd/server.go b/httpd/server.go index c3cfb1d9..1009f0ce 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -1092,6 +1092,9 @@ func (s *httpdServer) initializeRouter() { AllowedHosts: s.binding.Security.AllowedHosts, AllowedHostsAreRegex: s.binding.Security.AllowedHostsAreRegex, HostsProxyHeaders: s.binding.Security.HostsProxyHeaders, + SSLRedirect: s.binding.Security.HTTPSRedirect, + SSLHost: s.binding.Security.HTTPSHost, + SSLTemporaryRedirect: true, SSLProxyHeaders: s.binding.Security.getHTTPSProxyHeaders(), STSSeconds: s.binding.Security.STSSeconds, STSIncludeSubdomains: s.binding.Security.STSIncludeSubdomains, diff --git a/sftpgo.json b/sftpgo.json index 1b3303d7..80f17504 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -241,6 +241,8 @@ "allowed_hosts": [], "allowed_hosts_are_regex": false, "hosts_proxy_headers": [], + "https_redirect": false, + "https_host": "", "https_proxy_headers": [], "sts_seconds": 0, "sts_include_subdomains": false,