git.iko.soy/typeable/oauth2-proxy
on master help sign in
Implements --real-client-ip-header option. (#503)

* Implements -real-client-ip-header option. * The -real-client-ip-header determines what HTTP header is used for determining the "real client IP" of the remote client. * The -real-client-ip-header option supports the following headers: X-Forwarded-For X-ProxyUser-IP and X-Real-IP (default). * Introduces new realClientIPParser interface to allow for multiple polymorphic classes to decide how to determine the real client IP. * TODO: implement the more standard, but more complex `Forwarded` HTTP header. * Corrected order of expected/actual in test cases * Improved error message in getRemoteIP * Add tests for getRemoteIP and getClientString * Add comment explaining splitting of header * Update documentation on -real-client-ip-header w/o -reverse-proxy * Add PR number in changelog. * Fix typo repeated word: "it" Co-Authored-By: Joel Speed <Joel.speed@hotmail.co.uk> * Update extended configuration language * Simplify the language around dependance on -reverse-proxy Co-Authored-By: Joel Speed <Joel.speed@hotmail.co.uk> * Added completions * Reorder real client IP header options * Update CHANGELOG.md * Apply suggestions from code review Co-authored-by: Isabelle COWAN-BERGMAN <Izzette@users.noreply.github.com> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> Co-authored-by: Henry Jenkins <henry@henryjenkins.name>

commit111d17efdecd4f716bc49b3ba8755ff2096d8c2c authorIsabelle COWAN-BERGMAN <Izzette@users.noreply.github.com> dateTue 12 May 2020, 19:41 (6 years ago) parentd0cfca4
10 files changed+371−52
CHANGELOG.md+1 contrib/oauth2-proxy_autocomplete.sh+4 docs/configuration/configuration.md+1 main.go+1 oauthproxy.go+4−10 options.go+25−14 options_test.go+43 pkg/logger/logger.go+14−28 realclientip.go+102 realclientip_test.go+176
CHANGELOG.md+1
@@ -35,6 +35,7 @@
3535
3636## Changes since v5.1.1
3737
38+- [#503](https://github.com/oauth2-proxy/oauth2-proxy/pull/503) Implements --real-client-ip-header option to select the header from which to obtain a proxied client's IP (@Izzette)
3839- [#529](https://github.com/oauth2-proxy/oauth2-proxy/pull/529) Add local test environments for testing changes and new features (@JoelSpeed)
3940- [#537](https://github.com/oauth2-proxy/oauth2-proxy/pull/537) Drop Fallback to Email if User not set (@JoelSpeed)
4041- [#535](https://github.com/oauth2-proxy/oauth2-proxy/pull/535) Drop support for pre v3.1 cookies (@JoelSpeed)
contrib/oauth2-proxy_autocomplete.sh+4
@@ -20,6 +20,10 @@ _oauth2_proxy() {
2020 COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov digitalocean" -- ${cur}) )
2121 return 0
2222 ;;
23+ --real-client-ip-header)
24+ COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
25+ return 0
26+ ;;
2327 -@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
2428 return 0
2529 ;;
docs/configuration/configuration.md+1
@@ -90,6 +90,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
9090| `--proxy-prefix` | string | the url root path that this proxy should be nested under (e.g. /`<oauth2>/sign_in`) | `"/oauth2"` |
9191| `--proxy-websockets` | bool | enables WebSocket proxying | true |
9292| `--pubjwk-url` | string | JWK pubkey access endpoint: required by login.gov | |
93+| `--real-client-ip-header` | string | Header used to determine the real IP of the client, requires `--reverse-proxy` to be set (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP) | X-Real-IP |
9394| `--redeem-url` | string | Token redemption endpoint | |
9495| `--redirect-url` | string | the OAuth Redirect URL. ie: `"https://internalapp.yourcompany.com/oauth2/callback"` | |
9596| `--redis-cluster-connection-urls` | string \| list | List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with `--redis-use-cluster` | |
main.go+1
@@ -26,6 +26,7 @@ func main() {
2626 flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients")
2727 flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
2828 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
29+ flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP)")
2930 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
3031 flagSet.String("tls-cert-file", "", "path to certificate file")
3132 flagSet.String("tls-key-file", "", "path to private key file")
oauthproxy.go+4−10
@@ -112,6 +112,7 @@ type OAuthProxy struct {
112112 jwtBearerVerifiers []*oidc.IDTokenVerifier
113113 compiledRegex []*regexp.Regexp
114114 templates *template.Template
115+ realClientIPParser realClientIPParser
115116 Banner string
116117 Footer string
117118}
@@ -308,6 +309,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
308309 skipJwtBearerTokens: opts.SkipJwtBearerTokens,
309310 jwtBearerVerifiers: opts.jwtBearerVerifiers,
310311 compiledRegex: opts.compiledRegex,
312+ realClientIPParser: opts.realClientIPParser,
311313 SetXAuthRequest: opts.SetXAuthRequest,
312314 PassBasicAuth: opts.PassBasicAuth,
313315 SetBasicAuth: opts.SetBasicAuth,
@@ -636,14 +638,6 @@ func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
636638 return false
637639}
638640
639−func getRemoteAddr(req *http.Request) (s string) {
640− s = req.RemoteAddr
641− if req.Header.Get("X-Real-IP") != "" {
642− s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
643− }
644− return
645−}
646−
647641// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
648642var noCacheHeaders = map[string]string{
649643 "Expires": time.Unix(0, 0).Format(time.RFC1123),
@@ -766,7 +760,7 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
766760// OAuthCallback is the OAuth2 authentication flow callback that finishes the
767761// OAuth2 authentication flow
768762func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
769− remoteAddr := getRemoteAddr(req)
763+ remoteAddr := getClientString(p.realClientIPParser, req, true)
770764
771765 // finish the oauth cycle
772766 err := req.ParseForm()
@@ -894,7 +888,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
894888 }
895889 }
896890
897− remoteAddr := getRemoteAddr(req)
891+ remoteAddr := getClientString(p.realClientIPParser, req, true)
898892 if session == nil {
899893 session, err = p.LoadCookiedSession(req)
900894 if err != nil {
options.go+25−14
@@ -31,19 +31,20 @@ import (
3131// Options holds Configuration Options that can be set by Command Line Flag,
3232// or Config File
3333type Options struct {
34− ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
35− PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
36− ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
37− HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
38− HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
39− ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"`
40− ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"`
41− RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
42− ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
43− ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
44− ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"`
45− TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
46− TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
34+ ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
35+ PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
36+ ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
37+ HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
38+ HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
39+ ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"`
40+ RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header" env:"OAUTH2_PROXY_REAL_CLIENT_IP_HEADER"`
41+ ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"`
42+ RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
43+ ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
44+ ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
45+ ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"`
46+ TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
47+ TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
4748
4849 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
4950 KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
@@ -139,6 +140,7 @@ type Options struct {
139140 signatureData *SignatureData
140141 oidcVerifier *oidc.IDTokenVerifier
141142 jwtBearerVerifiers []*oidc.IDTokenVerifier
143+ realClientIPParser realClientIPParser
142144}
143145
144146// SignatureData holds hmacauth signature hash and key
@@ -456,6 +458,13 @@ func (o *Options) Validate() error {
456458 msgs = validateCookieName(o, msgs)
457459 msgs = setupLogger(o, msgs)
458460
461+ if o.ReverseProxy {
462+ o.realClientIPParser, err = getRealClientIPParser(o.RealClientIPHeader)
463+ if err != nil {
464+ msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.RealClientIPHeader, err))
465+ }
466+ }
467+
459468 if len(msgs) != 0 {
460469 return fmt.Errorf("invalid configuration:\n %s",
461470 strings.Join(msgs, "\n "))
@@ -695,7 +704,9 @@ func setupLogger(o *Options, msgs []string) []string {
695704 logger.SetStandardTemplate(o.StandardLoggingFormat)
696705 logger.SetAuthTemplate(o.AuthLoggingFormat)
697706 logger.SetReqTemplate(o.RequestLoggingFormat)
698− logger.SetReverseProxy(o.ReverseProxy)
707+ logger.SetGetClientFunc(func(r *http.Request) string {
708+ return getClientString(o.realClientIPParser, r, false)
709+ })
699710
700711 excludePaths := make([]string, 0)
701712 excludePaths = append(excludePaths, strings.Split(o.ExcludeLoggingPaths, ",")...)
options_test.go+43
@@ -326,3 +326,46 @@ func TestGCPHealthcheck(t *testing.T) {
326326 o.GCPHealthChecks = true
327327 assert.Equal(t, nil, o.Validate())
328328}
329+
330+func TestRealClientIPHeader(t *testing.T) {
331+ var o *Options
332+ var err error
333+ var expected string
334+
335+ // Ensure nil if ReverseProxy not set.
336+ o = testOptions()
337+ o.RealClientIPHeader = "X-Real-IP"
338+ assert.Equal(t, nil, o.Validate())
339+ assert.Nil(t, o.realClientIPParser)
340+
341+ // Ensure simple use case works.
342+ o = testOptions()
343+ o.ReverseProxy = true
344+ o.RealClientIPHeader = "X-Forwarded-For"
345+ assert.Equal(t, nil, o.Validate())
346+ assert.NotNil(t, o.realClientIPParser)
347+
348+ // Ensure unknown header format process an error.
349+ o = testOptions()
350+ o.ReverseProxy = true
351+ o.RealClientIPHeader = "Forwarded"
352+ err = o.Validate()
353+ assert.NotEqual(t, nil, err)
354+ expected = errorMsg([]string{
355+ "real_client_ip_header (Forwarded) not accepted parameter value: the http header key (Forwarded) is either invalid or unsupported",
356+ })
357+ assert.Equal(t, expected, err.Error())
358+ assert.Nil(t, o.realClientIPParser)
359+
360+ // Ensure invalid header format produces an error.
361+ o = testOptions()
362+ o.ReverseProxy = true
363+ o.RealClientIPHeader = "!934invalidheader-23:"
364+ err = o.Validate()
365+ assert.NotEqual(t, nil, err)
366+ expected = errorMsg([]string{
367+ "real_client_ip_header (!934invalidheader-23:) not accepted parameter value: the http header key (!934invalidheader-23:) is either invalid or unsupported",
368+ })
369+ assert.Equal(t, expected, err.Error())
370+ assert.Nil(t, o.realClientIPParser)
371+}
pkg/logger/logger.go+14−28
@@ -3,7 +3,6 @@ package logger
33import (
44 "fmt"
55 "io"
6− "net"
76 "net/http"
87 "net/url"
98 "os"
@@ -76,6 +75,9 @@ type reqLogMessageData struct {
7675 Username string
7776}
7877
78+// Returns the apparent "real client IP" as a string.
79+type GetClientFunc = func(r *http.Request) string
80+
7981// A Logger represents an active logging object that generates lines of
8082// output to an io.Writer passed through a formatter. Each logging
8183// operation makes a single call to the Writer's Write method. A Logger
@@ -88,7 +90,7 @@ type Logger struct {
8890 stdEnabled bool
8991 authEnabled bool
9092 reqEnabled bool
91− reverseProxy bool
93+ getClientFunc GetClientFunc
9294 excludePaths map[string]struct{}
9395 stdLogTemplate *template.Template
9496 authTemplate *template.Template
@@ -103,7 +105,7 @@ func New(flag int) *Logger {
103105 stdEnabled: true,
104106 authEnabled: true,
105107 reqEnabled: true,
106− reverseProxy: false,
108+ getClientFunc: func(r *http.Request) string { return r.RemoteAddr },
107109 excludePaths: nil,
108110 stdLogTemplate: template.Must(template.New("std-log").Parse(DefaultStandardLoggingFormat)),
109111 authTemplate: template.Must(template.New("auth-log").Parse(DefaultAuthLoggingFormat)),
@@ -153,7 +155,7 @@ func (l *Logger) PrintAuthf(username string, req *http.Request, status AuthStatu
153155 username = "-"
154156 }
155157
156− client := GetClient(req, l.reverseProxy)
158+ client := l.getClientFunc(req)
157159
158160 l.mu.Lock()
159161 defer l.mu.Unlock()
@@ -201,7 +203,7 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.
201203 }
202204 }
203205
204− client := GetClient(req, l.reverseProxy)
206+ client := l.getClientFunc(req)
205207
206208 l.mu.Lock()
207209 defer l.mu.Unlock()
@@ -252,22 +254,6 @@ func (l *Logger) GetFileLineString(calldepth int) string {
252254 return fmt.Sprintf("%s:%d", file, line)
253255}
254256
255−// GetClient parses an HTTP request for the client/remote IP address.
256−func GetClient(req *http.Request, reverseProxy bool) string {
257− client := req.RemoteAddr
258− if reverseProxy {
259− if ip := req.Header.Get("X-Real-IP"); ip != "" {
260− client = ip
261− }
262− }
263−
264− if c, _, err := net.SplitHostPort(client); err == nil {
265− client = c
266− }
267−
268− return client
269−}
270−
271257// FormatTimestamp returns a formatted timestamp.
272258func (l *Logger) FormatTimestamp(ts time.Time) string {
273259 if l.flag&LUTC != 0 {
@@ -312,11 +298,11 @@ func (l *Logger) SetReqEnabled(e bool) {
312298 l.reqEnabled = e
313299}
314300
315−// SetReverseProxy controls whether logging will trust headers that can be set by a reverse proxy.
316−func (l *Logger) SetReverseProxy(e bool) {
301+// SetGetClientFunc sets the function which determines the apparent "real client IP".
302+func (l *Logger) SetGetClientFunc(f GetClientFunc) {
317303 l.mu.Lock()
318304 defer l.mu.Unlock()
319− l.reverseProxy = e
305+ l.getClientFunc = f
320306}
321307
322308// SetExcludePaths sets the paths to exclude from logging.
@@ -392,10 +378,10 @@ func SetReqEnabled(e bool) {
392378 std.SetReqEnabled(e)
393379}
394380
395−// SetReverseProxy controls whether logging will trust headers that can be set
396−// by a reverse proxy for the standard logger.
397−func SetReverseProxy(e bool) {
398− std.SetReverseProxy(e)
381+// SetGetClientFunc sets the function which determines the apparent IP address
382+// set by a reverse proxy for the standard logger.
383+func SetGetClientFunc(f GetClientFunc) {
384+ std.SetGetClientFunc(f)
399385}
400386
401387// SetExcludePaths sets the path to exclude from logging, eg: health checks
realclientip.gonew file+102
@@ -0,0 +1,102 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "net"
6+ "net/http"
7+ "strings"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
10+)
11+
12+type realClientIPParser interface {
13+ GetRealClientIP(http.Header) (net.IP, error)
14+}
15+
16+func getRealClientIPParser(headerKey string) (realClientIPParser, error) {
17+ headerKey = http.CanonicalHeaderKey(headerKey)
18+
19+ switch headerKey {
20+ case http.CanonicalHeaderKey("X-Forwarded-For"), http.CanonicalHeaderKey("X-Real-IP"), http.CanonicalHeaderKey("X-ProxyUser-IP"):
21+ return &xForwardedForClientIPParser{header: headerKey}, nil
22+ }
23+
24+ // TODO: implement the more standardized but more complex `Forwarded` header.
25+ return nil, fmt.Errorf("the http header key (%s) is either invalid or unsupported", headerKey)
26+}
27+
28+type xForwardedForClientIPParser struct {
29+ header string
30+}
31+
32+// GetRealClientIP obtain the IP address of the end-user (not proxy).
33+// Parses headers sharing the format as specified by:
34+// * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For.
35+// Returns the `<client>` portion specified in the above document.
36+// Additionally, is capable of parsing IPs with the port included, for v4 in the format "<ip>:<port>" and for v6 in the
37+// format "[<ip>]:<port>". With-port and without-port formats are seamlessly supported concurrently.
38+func (p xForwardedForClientIPParser) GetRealClientIP(h http.Header) (net.IP, error) {
39+ var ipStr string
40+ if realIP := h.Get(p.header); realIP != "" {
41+ ipStr = realIP
42+ } else {
43+ return nil, nil
44+ }
45+
46+ // Each successive proxy may append itself, comma separated, to the end of the X-Forwarded-for header.
47+ // Select only the first IP listed, as it is the client IP recorded by the first proxy.
48+ if commaIndex := strings.IndexRune(ipStr, ','); commaIndex != -1 {
49+ ipStr = ipStr[:commaIndex]
50+ }
51+ ipStr = strings.TrimSpace(ipStr)
52+
53+ if ipHost, _, err := net.SplitHostPort(ipStr); err == nil {
54+ ipStr = ipHost
55+ }
56+
57+ ip := net.ParseIP(ipStr)
58+ if ip == nil {
59+ return nil, fmt.Errorf("unable to parse ip (%s) from %s header", ipStr, http.CanonicalHeaderKey(p.header))
60+ }
61+
62+ return ip, nil
63+}
64+
65+// getRemoteIP obtains the IP of the low-level connected network host
66+func getRemoteIP(req *http.Request) (net.IP, error) {
67+ if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil {
68+ return nil, fmt.Errorf("unable to get ip and port from http.RemoteAddr (%s)", req.RemoteAddr)
69+ } else if ip := net.ParseIP(ipStr); ip != nil {
70+ return ip, nil
71+ } else {
72+ return nil, fmt.Errorf("unable to parse ip (%s)", ipStr)
73+ }
74+}
75+
76+// getClientString obtains the human readable string of the remote IP and optionally the real client IP if available
77+func getClientString(p realClientIPParser, req *http.Request, full bool) (s string) {
78+ var realClientIPStr string
79+ if p != nil {
80+ if realClientIP, err := p.GetRealClientIP(req.Header); err != nil {
81+ logger.Printf("Unable to get real client IP: %v", err)
82+ } else if realClientIP != nil {
83+ realClientIPStr = realClientIP.String()
84+ }
85+ }
86+
87+ var remoteIPStr string
88+ if remoteIP, err := getRemoteIP(req); err == nil {
89+ remoteIPStr = remoteIP.String()
90+ } else {
91+ // Should not happen, if it does, likely a bug.
92+ logger.Printf("Unable to get remote IP(?!?!): %v", err)
93+ }
94+
95+ if !full && realClientIPStr != "" {
96+ return realClientIPStr
97+ }
98+ if full && realClientIPStr != "" {
99+ return fmt.Sprintf("%s (%s)", remoteIPStr, realClientIPStr)
100+ }
101+ return remoteIPStr
102+}
realclientip_test.gonew file+176
@@ -0,0 +1,176 @@
1+package main
2+
3+import (
4+ "net"
5+ "net/http"
6+ "reflect"
7+ "testing"
8+
9+ "github.com/stretchr/testify/assert"
10+)
11+
12+func TestGetRealClientIPParser(t *testing.T) {
13+ forwardedForType := reflect.TypeOf((*xForwardedForClientIPParser)(nil))
14+
15+ tests := []struct {
16+ header string
17+ errString string
18+ parserType reflect.Type
19+ }{
20+ {"X-Forwarded-For", "", forwardedForType},
21+ {"X-REAL-IP", "", forwardedForType},
22+ {"x-proxyuser-ip", "", forwardedForType},
23+ {"", "the http header key () is either invalid or unsupported", nil},
24+ {"Forwarded", "the http header key (Forwarded) is either invalid or unsupported", nil},
25+ {"2#* @##$$:kd", "the http header key (2#* @##$$:kd) is either invalid or unsupported", nil},
26+ }
27+
28+ for _, test := range tests {
29+ p, err := getRealClientIPParser(test.header)
30+
31+ if test.errString == "" {
32+ assert.Nil(t, err)
33+ } else {
34+ assert.NotNil(t, err)
35+ assert.Equal(t, test.errString, err.Error())
36+ }
37+
38+ if test.parserType == nil {
39+ assert.Nil(t, p)
40+ } else {
41+ assert.NotNil(t, p)
42+ assert.Equal(t, test.parserType, reflect.TypeOf(p))
43+ }
44+
45+ if xp, ok := p.(*xForwardedForClientIPParser); ok {
46+ assert.Equal(t, http.CanonicalHeaderKey(test.header), xp.header)
47+ }
48+ }
49+}
50+
51+func TestXForwardedForClientIPParser(t *testing.T) {
52+ p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
53+
54+ tests := []struct {
55+ headerValue string
56+ errString string
57+ expectedIP net.IP
58+ }{
59+ {"", "", nil},
60+ {"1.2.3.4", "", net.ParseIP("1.2.3.4")},
61+ {"10::23", "", net.ParseIP("10::23")},
62+ {"::1", "", net.ParseIP("::1")},
63+ {"[::1]:1234", "", net.ParseIP("::1")},
64+ {"10.0.10.11:1234", "", net.ParseIP("10.0.10.11")},
65+ {"192.168.10.50, 10.0.0.1, 1.2.3.4", "", net.ParseIP("192.168.10.50")},
66+ {"nil", "unable to parse ip (nil) from X-Forwarded-For header", nil},
67+ {"10000.10000.10000.10000", "unable to parse ip (10000.10000.10000.10000) from X-Forwarded-For header", nil},
68+ }
69+
70+ for _, test := range tests {
71+ h := http.Header{}
72+ h.Add("X-Forwarded-For", test.headerValue)
73+
74+ ip, err := p.GetRealClientIP(h)
75+
76+ if test.errString == "" {
77+ assert.Nil(t, err)
78+ } else {
79+ assert.NotNil(t, err)
80+ assert.Equal(t, test.errString, err.Error())
81+ }
82+
83+ if test.expectedIP == nil {
84+ assert.Nil(t, ip)
85+ } else {
86+ assert.NotNil(t, ip)
87+ assert.Equal(t, test.expectedIP, ip)
88+ }
89+ }
90+}
91+
92+func TestXForwardedForClientIPParserIgnoresOthers(t *testing.T) {
93+ p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
94+
95+ h := http.Header{}
96+ expectedIPString := "192.168.10.50"
97+ h.Add("X-Real-IP", "10.0.0.1")
98+ h.Add("X-ProxyUser-IP", "10.0.0.1")
99+ h.Add("X-Forwarded-For", expectedIPString)
100+ ip, err := p.GetRealClientIP(h)
101+ assert.Nil(t, err)
102+ assert.NotNil(t, ip)
103+ assert.Equal(t, ip, net.ParseIP(expectedIPString))
104+}
105+
106+func TestGetRemoteIP(t *testing.T) {
107+ tests := []struct {
108+ remoteAddr string
109+ errString string
110+ expectedIP net.IP
111+ }{
112+ {"", "unable to get ip and port from http.RemoteAddr ()", nil},
113+ {"nil", "unable to get ip and port from http.RemoteAddr (nil)", nil},
114+ {"235.28.129.186", "unable to get ip and port from http.RemoteAddr (235.28.129.186)", nil},
115+ {"90::45", "unable to get ip and port from http.RemoteAddr (90::45)", nil},
116+ {"192.168.73.165:14976, 10.4.201.15:18453", "unable to get ip and port from http.RemoteAddr (192.168.73.165:14976, 10.4.201.15:18453)", nil},
117+ {"10000.10000.10000.10000:8080", "unable to parse ip (10000.10000.10000.10000)", nil},
118+ {"[::1]:48290", "", net.ParseIP("::1")},
119+ {"10.254.244.165:62750", "", net.ParseIP("10.254.244.165")},
120+ }
121+
122+ for _, test := range tests {
123+ req := &http.Request{RemoteAddr: test.remoteAddr}
124+
125+ ip, err := getRemoteIP(req)
126+
127+ if test.errString == "" {
128+ assert.Nil(t, err)
129+ } else {
130+ assert.NotNil(t, err)
131+ assert.Equal(t, test.errString, err.Error())
132+ }
133+
134+ if test.expectedIP == nil {
135+ assert.Nil(t, ip)
136+ } else {
137+ assert.NotNil(t, ip)
138+ assert.Equal(t, test.expectedIP, ip)
139+ }
140+ }
141+}
142+
143+func TestGetClientString(t *testing.T) {
144+ p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
145+
146+ tests := []struct {
147+ parser realClientIPParser
148+ remoteAddr string
149+ headerValue string
150+ expectedClient string
151+ expectedClientFull string
152+ }{
153+ // Should fail quietly, only printing warnings to the log
154+ {nil, "", "", "", ""},
155+ {p, "127.0.0.1:11950", "", "127.0.0.1", "127.0.0.1"},
156+ {p, "[::1]:28660", "99.103.56.12", "99.103.56.12", "::1 (99.103.56.12)"},
157+ {nil, "10.254.244.165:62750", "", "10.254.244.165", "10.254.244.165"},
158+ // Parser is nil, the contents of X-Forwarded-For should be ignored in all cases.
159+ {nil, "[2001:470:26:307:a5a1:1177:2ae3:e9c3]:48290", "127.0.0.1", "2001:470:26:307:a5a1:1177:2ae3:e9c3", "2001:470:26:307:a5a1:1177:2ae3:e9c3"},
160+ }
161+
162+ for _, test := range tests {
163+ h := http.Header{}
164+ h.Add("X-Forwarded-For", test.headerValue)
165+ req := &http.Request{
166+ Header: h,
167+ RemoteAddr: test.remoteAddr,
168+ }
169+
170+ client := getClientString(test.parser, req, false)
171+ assert.Equal(t, test.expectedClient, client)
172+
173+ clientFull := getClientString(test.parser, req, true)
174+ assert.Equal(t, test.expectedClientFull, clientFull)
175+ }
176+}