diff --git a/config/config.go b/config/config.go index 9946cc63..9d60ceba 100644 --- a/config/config.go +++ b/config/config.go @@ -67,16 +67,18 @@ var ( Debug: false, } defaultWebDAVDBinding = webdavd.Binding{ - Address: "", - Port: 0, - EnableHTTPS: false, - CertificateFile: "", - CertificateKeyFile: "", - MinTLSVersion: 12, - ClientAuthType: 0, - TLSCipherSuites: nil, - Prefix: "", - ProxyAllowed: nil, + Address: "", + Port: 0, + EnableHTTPS: false, + CertificateFile: "", + CertificateKeyFile: "", + MinTLSVersion: 12, + ClientAuthType: 0, + TLSCipherSuites: nil, + Prefix: "", + ProxyAllowed: nil, + ClientIPProxyHeader: "", + ClientIPHeaderDepth: 0, } defaultHTTPDBinding = httpd.Binding{ Address: "", @@ -90,6 +92,8 @@ var ( ClientAuthType: 0, TLSCipherSuites: nil, ProxyAllowed: nil, + ClientIPProxyHeader: "", + ClientIPHeaderDepth: 0, HideLoginURL: 0, RenderOpenAPI: true, 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) { binding := webdavd.Binding{ MinTLSVersion: 12, @@ -1184,9 +1212,7 @@ func getWebDAVDBindingFromEnv(idx int) { isSet = true } - proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx)) - if ok { - binding.ProxyAllowed = proxyAllowed + if getWebDAVDBindingProxyConfigsFromEnv(idx, &binding) { isSet = true } @@ -1519,6 +1545,30 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool { 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) { binding := getDefaultHTTPBinding(idx) isSet := false @@ -1589,9 +1639,7 @@ func getHTTPDBindingFromEnv(idx int) { isSet = true } - proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx)) - if ok { - binding.ProxyAllowed = proxyAllowed + if getHTTPDBindingProxyConfigsFromEnv(idx, &binding) { isSet = true } diff --git a/config/config_test.go b/config/config_test.go index 3e36f29f..c9228a00 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -837,6 +837,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) { 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__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__PORT", "9000") 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__TLS_CIPHER_SUITES") 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__PORT") 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.Len(t, bindings[0].TLSCipherSuites, 0) require.Empty(t, bindings[0].Prefix) + require.Equal(t, 0, bindings[0].ClientIPHeaderDepth) require.Equal(t, 8000, bindings[1].Port) require.Equal(t, "127.0.0.1", bindings[1].Address) require.False(t, bindings[1].EnableHTTPS) @@ -881,6 +886,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) { require.Len(t, bindings[1].TLSCipherSuites, 1) 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, "X-Forwarded-For", bindings[1].ClientIPProxyHeader) + require.Equal(t, 2, bindings[1].ClientIPHeaderDepth) require.Empty(t, bindings[1].Prefix) require.Equal(t, 9000, bindings[2].Port) 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, "webdav.crt", bindings[2].CertificateFile) require.Equal(t, "webdav.key", bindings[2].CertificateKeyFile) + require.Equal(t, 0, bindings[2].ClientIPHeaderDepth) } 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__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__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__WEB_CLIENT_INTEGRATIONS__1__URL", "http://127.0.0.1/") 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__TLS_CIPHER_SUITES") 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__WEB_CLIENT_INTEGRATIONS__1__URL") 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, 0, bindings[0].HideLoginURL) require.False(t, bindings[0].Security.Enabled) + require.Equal(t, 0, bindings[0].ClientIPHeaderDepth) require.Equal(t, 8000, bindings[1].Port) require.Equal(t, "127.0.0.1", bindings[1].Address) require.False(t, bindings[1].EnableHTTPS) @@ -1051,6 +1064,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.False(t, bindings[1].Security.Enabled) require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name) 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, "127.0.1.1", bindings[2].Address) require.True(t, bindings[2].EnableHTTPS) @@ -1065,6 +1079,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Len(t, bindings[2].ProxyAllowed, 2) 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, "X-Real-IP", bindings[2].ClientIPProxyHeader) + require.Equal(t, 2, bindings[2].ClientIPHeaderDepth) require.Equal(t, 3, bindings[2].HideLoginURL) require.Len(t, bindings[2].WebClientIntegrations, 1) require.Equal(t, "http://127.0.0.1/", bindings[2].WebClientIntegrations[0].URL) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index c23c6ae0..cf8df9ed 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -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. - `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: "". - - `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_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. @@ -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. - `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. - - `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. - - `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. + - `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 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. - `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: diff --git a/httpd/httpd.go b/httpd/httpd.go index 2486154f..fd151dc2 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -417,9 +417,16 @@ type Binding struct { // any invalid name will be silently ignored. // The order matters, the ciphers listed first will be the preferred ones. 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, - // X-Forwarded-Proto headers. + // List of IP addresses and IP ranges allowed to set client IP proxy headers and + // X-Forwarded-Proto header. 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 // 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 diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 87b3fb1b..fa054163 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -1551,11 +1551,12 @@ func TestProxyHeaders(t *testing.T) { testIP := "10.29.1.9" validForwardedFor := "172.19.2.6" b := Binding{ - Address: "", - Port: 8080, - EnableWebAdmin: true, - EnableWebClient: false, - ProxyAllowed: []string{testIP, "10.8.0.0/30"}, + Address: "", + Port: 8080, + EnableWebAdmin: true, + EnableWebClient: false, + ProxyAllowed: []string{testIP, "10.8.0.0/30"}, + ClientIPProxyHeader: "x-forwarded-for", } err = b.parseAllowedProxy() assert.NoError(t, err) diff --git a/httpd/server.go b/httpd/server.go index 96c61068..39ed5842 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -977,7 +977,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler { if ip != nil { for _, allow := range s.binding.allowHeadersFrom { if allow(ip) { - parsedIP := util.GetRealIP(r) + parsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth) if parsedIP != "" { ipAddr = parsedIP r.RemoteAddr = ipAddr diff --git a/sftpgo.json b/sftpgo.json index 9931652a..6ca9ae41 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -149,7 +149,9 @@ "client_auth_type": 0, "tls_cipher_suites": [], "prefix": "", - "proxy_allowed": [] + "proxy_allowed": [], + "client_ip_proxy_header": "", + "client_ip_header_depth": 0 } ], "certificate_file": "", @@ -251,6 +253,8 @@ "client_auth_type": 0, "tls_cipher_suites": [], "proxy_allowed": [], + "client_ip_proxy_header": "", + "client_ip_header_depth": 0, "hide_login_url": 0, "render_openapi": true, "web_client_integrations": [], diff --git a/util/util.go b/util/util.go index e702372c..9dccdded 100644 --- a/util/util.go +++ b/util/util.go @@ -41,11 +41,7 @@ const ( ) var ( - xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") - 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}])))\\.?$") + 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. @@ -536,27 +532,29 @@ func GetSSHPublicKeyAsString(pubKey []byte) (string, error) { // GetRealIP returns the ip address as result of parsing either the // X-Real-IP header or the X-Forwarded-For header -func GetRealIP(r *http.Request) string { - var ip string - - 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 { +func GetRealIP(r *http.Request, header string, depth int) string { + if header == "" { 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 diff --git a/webdavd/internal_test.go b/webdavd/internal_test.go index 27546061..ce544634 100644 --- a/webdavd/internal_test.go +++ b/webdavd/internal_test.go @@ -435,19 +435,26 @@ func TestRemoteAddress(t *testing.T) { assert.NoError(t, err) assert.Empty(t, req.RemoteAddr) - req.Header.Set("True-Client-IP", remoteAddr1) - ip := util.GetRealIP(req) + trueClientIP := "True-Client-IP" + 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) - req.Header.Del("True-Client-IP") - req.Header.Set("CF-Connecting-IP", remoteAddr1) - ip = util.GetRealIP(req) + ip = util.GetRealIP(req, trueClientIP, 2) + assert.Empty(t, ip) + req.Header.Del(trueClientIP) + req.Header.Set(cfConnectingIP, remoteAddr1) + ip = util.GetRealIP(req, cfConnectingIP, 0) assert.Equal(t, remoteAddr1, ip) - req.Header.Del("CF-Connecting-IP") - req.Header.Set("X-Forwarded-For", remoteAddr1) - ip = util.GetRealIP(req) + req.Header.Del(cfConnectingIP) + req.Header.Set(xff, remoteAddr1) + ip = util.GetRealIP(req, xff, 0) assert.Equal(t, remoteAddr1, ip) // 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 ip = server.checkRemoteAddress(req) assert.Equal(t, remoteAddr1, ip) @@ -455,32 +462,41 @@ func TestRemoteAddress(t *testing.T) { ip = server.checkRemoteAddress(req) assert.Empty(t, ip) - req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v, %v", remoteAddr2, remoteAddr1)) - ip = util.GetRealIP(req) + req.Header.Set(xff, fmt.Sprintf("%v , %v", remoteAddr2, remoteAddr1)) + ip = util.GetRealIP(req, xff, 1) assert.Equal(t, remoteAddr2, ip) 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) assert.Equal(t, "12.34.56.78", ip) 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.Header.Set("X-Forwarded-For", remoteAddr1) + req.Header.Set(xff, remoteAddr1) ip = server.checkRemoteAddress(req) assert.Equal(t, remoteAddr1, ip) assert.Equal(t, ip, req.RemoteAddr) 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) assert.Equal(t, "10.8.0.3", ip) assert.Equal(t, ip, req.RemoteAddr) - req.Header.Del("X-Forwarded-For") + req.Header.Del(xff) req.RemoteAddr = "" - req.Header.Set("X-Real-IP", remoteAddr1) - ip = util.GetRealIP(req) + req.Header.Set(xRealIP, remoteAddr1) + ip = util.GetRealIP(req, "x-real-ip", 0) assert.Equal(t, remoteAddr1, ip) req.RemoteAddr = "" } diff --git a/webdavd/server.go b/webdavd/server.go index 5283a682..07490ba5 100644 --- a/webdavd/server.go +++ b/webdavd/server.go @@ -335,7 +335,7 @@ func (s *webDavServer) checkRemoteAddress(r *http.Request) string { if ip != nil { for _, allow := range s.binding.allowHeadersFrom { if allow(ip) { - parsedIP := util.GetRealIP(r) + parsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth) if parsedIP != "" { ipAddr = parsedIP r.RemoteAddr = ipAddr diff --git a/webdavd/webdavd.go b/webdavd/webdavd.go index 57ca3384..c73d0118 100644 --- a/webdavd/webdavd.go +++ b/webdavd/webdavd.go @@ -96,9 +96,16 @@ type Binding struct { // Prefix for WebDAV resources, if empty WebDAV resources will be available at the // root ("/") URI. If defined it must be an absolute URI. Prefix string `json:"prefix" mapstructure:"prefix"` - // List of IP addresses and IP ranges allowed to set X-Forwarded-For/X-Real-IP headers. - ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"` - allowHeadersFrom []func(net.IP) bool + // List of IP addresses and IP ranges allowed to set client IP proxy headers + 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"` + allowHeadersFrom []func(net.IP) bool } func (b *Binding) parseAllowedProxy() error {