httpd/webdav: allow to configure trusted proxy header and depth

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-05-28 19:47:23 +02:00
parent 32da923dfe
commit f6b11c2d01
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
11 changed files with 173 additions and 72 deletions

View file

@ -67,16 +67,18 @@ var (
Debug: false, Debug: false,
} }
defaultWebDAVDBinding = webdavd.Binding{ defaultWebDAVDBinding = webdavd.Binding{
Address: "", Address: "",
Port: 0, Port: 0,
EnableHTTPS: false, EnableHTTPS: false,
CertificateFile: "", CertificateFile: "",
CertificateKeyFile: "", CertificateKeyFile: "",
MinTLSVersion: 12, MinTLSVersion: 12,
ClientAuthType: 0, ClientAuthType: 0,
TLSCipherSuites: nil, TLSCipherSuites: nil,
Prefix: "", Prefix: "",
ProxyAllowed: nil, ProxyAllowed: nil,
ClientIPProxyHeader: "",
ClientIPHeaderDepth: 0,
} }
defaultHTTPDBinding = httpd.Binding{ defaultHTTPDBinding = httpd.Binding{
Address: "", Address: "",
@ -90,6 +92,8 @@ var (
ClientAuthType: 0, ClientAuthType: 0,
TLSCipherSuites: nil, TLSCipherSuites: nil,
ProxyAllowed: nil, ProxyAllowed: nil,
ClientIPProxyHeader: "",
ClientIPHeaderDepth: 0,
HideLoginURL: 0, HideLoginURL: 0,
RenderOpenAPI: true, RenderOpenAPI: true,
WebClientIntegrations: nil, WebClientIntegrations: nil,
@ -1126,6 +1130,30 @@ func applyFTPDBindingFromEnv(idx int, isSet bool, binding ftpd.Binding) {
} }
} }
func getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) bool {
isSet := false
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx))
if ok {
binding.ProxyAllowed = proxyAllowed
isSet = true
}
clientIPProxyHeader, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_PROXY_HEADER", idx))
if ok {
binding.ClientIPProxyHeader = clientIPProxyHeader
isSet = true
}
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx))
if ok {
binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
isSet = true
}
return isSet
}
func getWebDAVDBindingFromEnv(idx int) { func getWebDAVDBindingFromEnv(idx int) {
binding := webdavd.Binding{ binding := webdavd.Binding{
MinTLSVersion: 12, MinTLSVersion: 12,
@ -1184,9 +1212,7 @@ func getWebDAVDBindingFromEnv(idx int) {
isSet = true isSet = true
} }
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx)) if getWebDAVDBindingProxyConfigsFromEnv(idx, &binding) {
if ok {
binding.ProxyAllowed = proxyAllowed
isSet = true isSet = true
} }
@ -1519,6 +1545,30 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
return isSet return isSet
} }
func getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {
isSet := false
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx))
if ok {
binding.ProxyAllowed = proxyAllowed
isSet = true
}
clientIPProxyHeader, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_PROXY_HEADER", idx))
if ok {
binding.ClientIPProxyHeader = clientIPProxyHeader
isSet = true
}
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx))
if ok {
binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
isSet = true
}
return isSet
}
func getHTTPDBindingFromEnv(idx int) { func getHTTPDBindingFromEnv(idx int) {
binding := getDefaultHTTPBinding(idx) binding := getDefaultHTTPBinding(idx)
isSet := false isSet := false
@ -1589,9 +1639,7 @@ func getHTTPDBindingFromEnv(idx int) {
isSet = true isSet = true
} }
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx)) if getHTTPDBindingProxyConfigsFromEnv(idx, &binding) {
if ok {
binding.ProxyAllowed = proxyAllowed
isSet = true isSet = true
} }

View file

@ -837,6 +837,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0") os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES", "TLS_RSA_WITH_AES_128_CBC_SHA ") os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES", "TLS_RSA_WITH_AES_128_CBC_SHA ")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED", "192.168.10.1") os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED", "192.168.10.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER", "X-Forwarded-For")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH", "2")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1") os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000") os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1") os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1")
@ -852,6 +854,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
@ -873,6 +877,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.Equal(t, 12, bindings[0].MinTLSVersion) require.Equal(t, 12, bindings[0].MinTLSVersion)
require.Len(t, bindings[0].TLSCipherSuites, 0) require.Len(t, bindings[0].TLSCipherSuites, 0)
require.Empty(t, bindings[0].Prefix) require.Empty(t, bindings[0].Prefix)
require.Equal(t, 0, bindings[0].ClientIPHeaderDepth)
require.Equal(t, 8000, bindings[1].Port) require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address) require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS) require.False(t, bindings[1].EnableHTTPS)
@ -881,6 +886,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.Len(t, bindings[1].TLSCipherSuites, 1) require.Len(t, bindings[1].TLSCipherSuites, 1)
require.Equal(t, "TLS_RSA_WITH_AES_128_CBC_SHA", bindings[1].TLSCipherSuites[0]) require.Equal(t, "TLS_RSA_WITH_AES_128_CBC_SHA", bindings[1].TLSCipherSuites[0])
require.Equal(t, "192.168.10.1", bindings[1].ProxyAllowed[0]) require.Equal(t, "192.168.10.1", bindings[1].ProxyAllowed[0])
require.Equal(t, "X-Forwarded-For", bindings[1].ClientIPProxyHeader)
require.Equal(t, 2, bindings[1].ClientIPHeaderDepth)
require.Empty(t, bindings[1].Prefix) require.Empty(t, bindings[1].Prefix)
require.Equal(t, 9000, bindings[2].Port) require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address) require.Equal(t, "127.0.1.1", bindings[2].Address)
@ -891,6 +898,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.Equal(t, "/dav2", bindings[2].Prefix) require.Equal(t, "/dav2", bindings[2].Prefix)
require.Equal(t, "webdav.crt", bindings[2].CertificateFile) require.Equal(t, "webdav.crt", bindings[2].CertificateFile)
require.Equal(t, "webdav.key", bindings[2].CertificateKeyFile) require.Equal(t, "webdav.key", bindings[2].CertificateKeyFile)
require.Equal(t, 0, bindings[2].ClientIPHeaderDepth)
} }
func TestHTTPDBindingsFromEnv(t *testing.T) { func TestHTTPDBindingsFromEnv(t *testing.T) {
@ -917,6 +925,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED", " 192.168.9.1 , 172.16.25.0/24") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED", " 192.168.9.1 , 172.16.25.0/24")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER", "X-Real-IP")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH", "2")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL", "3") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL", "3")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__URL", "http://127.0.0.1/") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__URL", "http://127.0.0.1/")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__FILE_EXTENSIONS", ".pdf, .txt") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__FILE_EXTENSIONS", ".pdf, .txt")
@ -979,6 +989,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__URL") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__URL")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__FILE_EXTENSIONS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__FILE_EXTENSIONS")
@ -1038,6 +1050,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0]) require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0])
require.Equal(t, 0, bindings[0].HideLoginURL) require.Equal(t, 0, bindings[0].HideLoginURL)
require.False(t, bindings[0].Security.Enabled) require.False(t, bindings[0].Security.Enabled)
require.Equal(t, 0, bindings[0].ClientIPHeaderDepth)
require.Equal(t, 8000, bindings[1].Port) require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address) require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS) require.False(t, bindings[1].EnableHTTPS)
@ -1051,6 +1064,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.False(t, bindings[1].Security.Enabled) require.False(t, bindings[1].Security.Enabled)
require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name) require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name)
require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName) require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName)
require.Equal(t, 0, bindings[1].ClientIPHeaderDepth)
require.Equal(t, 9000, bindings[2].Port) require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address) require.Equal(t, "127.0.1.1", bindings[2].Address)
require.True(t, bindings[2].EnableHTTPS) require.True(t, bindings[2].EnableHTTPS)
@ -1065,6 +1079,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.Len(t, bindings[2].ProxyAllowed, 2) require.Len(t, bindings[2].ProxyAllowed, 2)
require.Equal(t, "192.168.9.1", bindings[2].ProxyAllowed[0]) require.Equal(t, "192.168.9.1", bindings[2].ProxyAllowed[0])
require.Equal(t, "172.16.25.0/24", bindings[2].ProxyAllowed[1]) require.Equal(t, "172.16.25.0/24", bindings[2].ProxyAllowed[1])
require.Equal(t, "X-Real-IP", bindings[2].ClientIPProxyHeader)
require.Equal(t, 2, bindings[2].ClientIPHeaderDepth)
require.Equal(t, 3, bindings[2].HideLoginURL) require.Equal(t, 3, bindings[2].HideLoginURL)
require.Len(t, bindings[2].WebClientIntegrations, 1) require.Len(t, bindings[2].WebClientIntegrations, 1)
require.Equal(t, "http://127.0.0.1/", bindings[2].WebClientIntegrations[0].URL) require.Equal(t, "http://127.0.0.1/", bindings[2].WebClientIntegrations[0].URL)

View file

@ -179,7 +179,9 @@ The configuration file contains the following sections:
- `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0. - `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty. - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `prefix`, string. Prefix for WebDAV resources, if empty WebDAV resources will be available at the `/` URI. If defined it must be an absolute URI, for example `/dav`. Default: "". - `prefix`, string. Prefix for WebDAV resources, if empty WebDAV resources will be available at the `/` URI. If defined it must be an absolute URI, for example `/dav`. Default: "".
- `proxy_allowed`, list of IP addresses and IP ranges allowed to set `X-Forwarded-For`, `X-Real-IP`, `CF-Connecting-IP`, `True-Client-IP` headers. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty. - `proxy_allowed`, list of IP addresses and IP ranges allowed to set client IP proxy header such as `X-Forwarded-For`. Any client IP proxy headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
- `client_ip_proxy_header`, string. Defines the allowed client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` etc. Default: empty
- `client_ip_header_depth`, integer. Some client IP headers such as `X-Forwarded-For` can contain multiple IP address, this setting define the position to trust starting from the right. For example if we have: `10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1` and the depth is `0`, SFTPGo will use `13.0.0.1` as client IP, if depth is `1`, `12.0.0.1` will be used and so on. Default: `0`.
- `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir. - `certificate_file`, string. Certificate for WebDAV over 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. A certificate and a private key are required to enable 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. - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and a private key are required to enable 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. - `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
@ -262,8 +264,10 @@ The configuration file contains the following sections:
- `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any. - `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
- `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`. - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to JWT/Web authentication. You need to define at least a certificate authority for this to work. Default: 0. - `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to JWT/Web authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: blank. - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `proxy_allowed`, list of IP addresses and IP ranges allowed to set `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto`, `CF-Connecting-IP`, `True-Client-IP` and any other headers defined in the `security` section. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: blank. - `proxy_allowed`, list of IP addresses and IP ranges allowed to set client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` and any other headers defined in the `security` section. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
- `client_ip_proxy_header`, string. Defines the allowed client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` etc. Default: empty
- `client_ip_header_depth`, integer. Some client IP headers such as `X-Forwarded-For` can contain multiple IP address, this setting define the position to trust starting from the right. For example if we have: `10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1` and the depth is `0`, SFTPGo will use `13.0.0.1` as client IP, if depth is `1`, `12.0.0.1` will be used and so on. Default: `0`.
- `hide_login_url`, integer. If both web admin and web client are enabled each login page will show a link to the other one. This setting allows to hide this link. 0 means that the login links are displayed on both admin and client login page. This is the default. 1 means that the login link to the web client login page is hidden on admin login page. 2 means that the login link to the web admin login page is hidden on client login page. The flags can be combined, for example 3 will disable both login links. - `hide_login_url`, integer. If both web admin and web client are enabled each login page will show a link to the other one. This setting allows to hide this link. 0 means that the login links are displayed on both admin and client login page. This is the default. 1 means that the login link to the web client login page is hidden on admin login page. 2 means that the login link to the web admin login page is hidden on client login page. The flags can be combined, for example 3 will disable both login links.
- `render_openapi`, boolean. Set to `false` to disable serving of the OpenAPI schema and renderer. Default `true`. - `render_openapi`, boolean. Set to `false` to disable serving of the OpenAPI schema and renderer. Default `true`.
- `web_client_integrations`, list of struct. The SFTPGo web client allows to send the files with the specified extensions to the configured URL using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This way you can integrate your own file viewer or editor. Take a look at the commentented example [here](../examples/webclient-integrations/test.html) to understand how to use this feature. Each struct has the following fields: - `web_client_integrations`, list of struct. The SFTPGo web client allows to send the files with the specified extensions to the configured URL using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This way you can integrate your own file viewer or editor. Take a look at the commentented example [here](../examples/webclient-integrations/test.html) to understand how to use this feature. Each struct has the following fields:

View file

@ -417,9 +417,16 @@ type Binding struct {
// any invalid name will be silently ignored. // any invalid name will be silently ignored.
// The order matters, the ciphers listed first will be the preferred ones. // The order matters, the ciphers listed first will be the preferred ones.
TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"` TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
// List of IP addresses and IP ranges allowed to set X-Forwarded-For, X-Real-IP, // List of IP addresses and IP ranges allowed to set client IP proxy headers and
// X-Forwarded-Proto headers. // X-Forwarded-Proto header.
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"` ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
// Allowed client IP proxy header such as "X-Forwarded-For", "X-Real-IP"
ClientIPProxyHeader string `json:"client_ip_proxy_header" mapstructure:"client_ip_proxy_header"`
// Some client IP headers such as "X-Forwarded-For" can contain multiple IP address, this setting
// define the position to trust starting from the right. For example if we have:
// "10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1" and the depth is 0, SFTPGo will use "13.0.0.1"
// as client IP, if depth is 1, "12.0.0.1" will be used and so on
ClientIPHeaderDepth int `json:"client_ip_header_depth" mapstructure:"client_ip_header_depth"`
// If both web admin and web client are enabled each login page will show a link // If both web admin and web client are enabled each login page will show a link
// to the other one. This setting allows to hide this link: // to the other one. This setting allows to hide this link:
// - 0 login links are displayed on both admin and client login page. This is the default // - 0 login links are displayed on both admin and client login page. This is the default

View file

@ -1551,11 +1551,12 @@ func TestProxyHeaders(t *testing.T) {
testIP := "10.29.1.9" testIP := "10.29.1.9"
validForwardedFor := "172.19.2.6" validForwardedFor := "172.19.2.6"
b := Binding{ b := Binding{
Address: "", Address: "",
Port: 8080, Port: 8080,
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: false, EnableWebClient: false,
ProxyAllowed: []string{testIP, "10.8.0.0/30"}, ProxyAllowed: []string{testIP, "10.8.0.0/30"},
ClientIPProxyHeader: "x-forwarded-for",
} }
err = b.parseAllowedProxy() err = b.parseAllowedProxy()
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -977,7 +977,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
if ip != nil { if ip != nil {
for _, allow := range s.binding.allowHeadersFrom { for _, allow := range s.binding.allowHeadersFrom {
if allow(ip) { if allow(ip) {
parsedIP := util.GetRealIP(r) parsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth)
if parsedIP != "" { if parsedIP != "" {
ipAddr = parsedIP ipAddr = parsedIP
r.RemoteAddr = ipAddr r.RemoteAddr = ipAddr

View file

@ -149,7 +149,9 @@
"client_auth_type": 0, "client_auth_type": 0,
"tls_cipher_suites": [], "tls_cipher_suites": [],
"prefix": "", "prefix": "",
"proxy_allowed": [] "proxy_allowed": [],
"client_ip_proxy_header": "",
"client_ip_header_depth": 0
} }
], ],
"certificate_file": "", "certificate_file": "",
@ -251,6 +253,8 @@
"client_auth_type": 0, "client_auth_type": 0,
"tls_cipher_suites": [], "tls_cipher_suites": [],
"proxy_allowed": [], "proxy_allowed": [],
"client_ip_proxy_header": "",
"client_ip_header_depth": 0,
"hide_login_url": 0, "hide_login_url": 0,
"render_openapi": true, "render_openapi": true,
"web_client_integrations": [], "web_client_integrations": [],

View file

@ -41,11 +41,7 @@ const (
) )
var ( var (
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
cfConnectingIP = http.CanonicalHeaderKey("CF-Connecting-IP")
trueClientIP = http.CanonicalHeaderKey("True-Client-IP")
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
) )
// Contains reports whether v is present in elems. // Contains reports whether v is present in elems.
@ -536,27 +532,29 @@ func GetSSHPublicKeyAsString(pubKey []byte) (string, error) {
// GetRealIP returns the ip address as result of parsing either the // GetRealIP returns the ip address as result of parsing either the
// X-Real-IP header or the X-Forwarded-For header // X-Real-IP header or the X-Forwarded-For header
func GetRealIP(r *http.Request) string { func GetRealIP(r *http.Request, header string, depth int) string {
var ip string if header == "" {
if clientIP := r.Header.Get(trueClientIP); clientIP != "" {
ip = clientIP
} else if xrip := r.Header.Get(xRealIP); xrip != "" {
ip = xrip
} else if clientIP := r.Header.Get(cfConnectingIP); clientIP != "" {
ip = clientIP
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
i := strings.Index(xff, ",")
if i == -1 {
i = len(xff)
}
ip = strings.TrimSpace(xff[:i])
}
if ip == "" || net.ParseIP(ip) == nil {
return "" return ""
} }
var ipAddresses []string
return ip for _, h := range r.Header.Values(header) {
for _, ipStr := range strings.Split(h, ",") {
ipStr = strings.TrimSpace(ipStr)
ipAddresses = append(ipAddresses, ipStr)
}
}
idx := len(ipAddresses) - 1 - depth
if idx >= 0 {
ip := strings.TrimSpace(ipAddresses[idx])
if ip == "" || net.ParseIP(ip) == nil {
return ""
}
return ip
}
return ""
} }
// GetHTTPLocalAddress returns the local address for an http.Request // GetHTTPLocalAddress returns the local address for an http.Request

View file

@ -435,19 +435,26 @@ func TestRemoteAddress(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Empty(t, req.RemoteAddr) assert.Empty(t, req.RemoteAddr)
req.Header.Set("True-Client-IP", remoteAddr1) trueClientIP := "True-Client-IP"
ip := util.GetRealIP(req) cfConnectingIP := "CF-Connecting-IP"
xff := "X-Forwarded-For"
xRealIP := "X-Real-IP"
req.Header.Set(trueClientIP, remoteAddr1)
ip := util.GetRealIP(req, trueClientIP, 0)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
req.Header.Del("True-Client-IP") ip = util.GetRealIP(req, trueClientIP, 2)
req.Header.Set("CF-Connecting-IP", remoteAddr1) assert.Empty(t, ip)
ip = util.GetRealIP(req) req.Header.Del(trueClientIP)
req.Header.Set(cfConnectingIP, remoteAddr1)
ip = util.GetRealIP(req, cfConnectingIP, 0)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
req.Header.Del("CF-Connecting-IP") req.Header.Del(cfConnectingIP)
req.Header.Set("X-Forwarded-For", remoteAddr1) req.Header.Set(xff, remoteAddr1)
ip = util.GetRealIP(req) ip = util.GetRealIP(req, xff, 0)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
// this will be ignored, remoteAddr1 is not allowed to se this header // this will be ignored, remoteAddr1 is not allowed to se this header
req.Header.Set("X-Forwarded-For", remoteAddr2) req.Header.Set(xff, remoteAddr2)
req.RemoteAddr = remoteAddr1 req.RemoteAddr = remoteAddr1
ip = server.checkRemoteAddress(req) ip = server.checkRemoteAddress(req)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
@ -455,32 +462,41 @@ func TestRemoteAddress(t *testing.T) {
ip = server.checkRemoteAddress(req) ip = server.checkRemoteAddress(req)
assert.Empty(t, ip) assert.Empty(t, ip)
req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v, %v", remoteAddr2, remoteAddr1)) req.Header.Set(xff, fmt.Sprintf("%v , %v", remoteAddr2, remoteAddr1))
ip = util.GetRealIP(req) ip = util.GetRealIP(req, xff, 1)
assert.Equal(t, remoteAddr2, ip) assert.Equal(t, remoteAddr2, ip)
req.RemoteAddr = remoteAddr2 req.RemoteAddr = remoteAddr2
req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v,%v", "12.34.56.78", "172.16.2.4")) req.Header.Set(xff, fmt.Sprintf("%v,%v", "12.34.56.78", "172.16.2.4"))
server.binding.ClientIPHeaderDepth = 1
server.binding.ClientIPProxyHeader = xff
ip = server.checkRemoteAddress(req) ip = server.checkRemoteAddress(req)
assert.Equal(t, "12.34.56.78", ip) assert.Equal(t, "12.34.56.78", ip)
assert.Equal(t, ip, req.RemoteAddr) assert.Equal(t, ip, req.RemoteAddr)
req.RemoteAddr = remoteAddr2
req.Header.Set(xff, fmt.Sprintf("%v,%v", "12.34.56.79", "172.16.2.5"))
server.binding.ClientIPHeaderDepth = 0
ip = server.checkRemoteAddress(req)
assert.Equal(t, "172.16.2.5", ip)
assert.Equal(t, ip, req.RemoteAddr)
req.RemoteAddr = "10.8.0.2" req.RemoteAddr = "10.8.0.2"
req.Header.Set("X-Forwarded-For", remoteAddr1) req.Header.Set(xff, remoteAddr1)
ip = server.checkRemoteAddress(req) ip = server.checkRemoteAddress(req)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
assert.Equal(t, ip, req.RemoteAddr) assert.Equal(t, ip, req.RemoteAddr)
req.RemoteAddr = "10.8.0.3" req.RemoteAddr = "10.8.0.3"
req.Header.Set("X-Forwarded-For", "not an ip") req.Header.Set(xff, "not an ip")
ip = server.checkRemoteAddress(req) ip = server.checkRemoteAddress(req)
assert.Equal(t, "10.8.0.3", ip) assert.Equal(t, "10.8.0.3", ip)
assert.Equal(t, ip, req.RemoteAddr) assert.Equal(t, ip, req.RemoteAddr)
req.Header.Del("X-Forwarded-For") req.Header.Del(xff)
req.RemoteAddr = "" req.RemoteAddr = ""
req.Header.Set("X-Real-IP", remoteAddr1) req.Header.Set(xRealIP, remoteAddr1)
ip = util.GetRealIP(req) ip = util.GetRealIP(req, "x-real-ip", 0)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
req.RemoteAddr = "" req.RemoteAddr = ""
} }

View file

@ -335,7 +335,7 @@ func (s *webDavServer) checkRemoteAddress(r *http.Request) string {
if ip != nil { if ip != nil {
for _, allow := range s.binding.allowHeadersFrom { for _, allow := range s.binding.allowHeadersFrom {
if allow(ip) { if allow(ip) {
parsedIP := util.GetRealIP(r) parsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth)
if parsedIP != "" { if parsedIP != "" {
ipAddr = parsedIP ipAddr = parsedIP
r.RemoteAddr = ipAddr r.RemoteAddr = ipAddr

View file

@ -96,9 +96,16 @@ type Binding struct {
// Prefix for WebDAV resources, if empty WebDAV resources will be available at the // Prefix for WebDAV resources, if empty WebDAV resources will be available at the
// root ("/") URI. If defined it must be an absolute URI. // root ("/") URI. If defined it must be an absolute URI.
Prefix string `json:"prefix" mapstructure:"prefix"` Prefix string `json:"prefix" mapstructure:"prefix"`
// List of IP addresses and IP ranges allowed to set X-Forwarded-For/X-Real-IP headers. // List of IP addresses and IP ranges allowed to set client IP proxy headers
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"` ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
allowHeadersFrom []func(net.IP) bool // Allowed client IP proxy header such as "X-Forwarded-For", "X-Real-IP"
ClientIPProxyHeader string `json:"client_ip_proxy_header" mapstructure:"client_ip_proxy_header"`
// Some client IP headers such as "X-Forwarded-For" can contain multiple IP address, this setting
// define the position to trust starting from the right. For example if we have:
// "10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1" and the depth is 0, SFTPGo will use "13.0.0.1"
// as client IP, if depth is 1, "12.0.0.1" will be used and so on
ClientIPHeaderDepth int `json:"client_ip_header_depth" mapstructure:"client_ip_header_depth"`
allowHeadersFrom []func(net.IP) bool
} }
func (b *Binding) parseAllowedProxy() error { func (b *Binding) parseAllowedProxy() error {