package main import ( "fmt" "net" "net/http" "strings" "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) type realClientIPParser interface { GetRealClientIP(http.Header) (net.IP, error) } func getRealClientIPParser(headerKey string) (realClientIPParser, error) { headerKey = http.CanonicalHeaderKey(headerKey) switch headerKey { case http.CanonicalHeaderKey("X-Forwarded-For"), http.CanonicalHeaderKey("X-Real-IP"), http.CanonicalHeaderKey("X-ProxyUser-IP"): return &xForwardedForClientIPParser{header: headerKey}, nil } // TODO: implement the more standardized but more complex `Forwarded` header. return nil, fmt.Errorf("the http header key (%s) is either invalid or unsupported", headerKey) } type xForwardedForClientIPParser struct { header string } // GetRealClientIP obtain the IP address of the end-user (not proxy). // Parses headers sharing the format as specified by: // * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For. // Returns the `` portion specified in the above document. // Additionally, is capable of parsing IPs with the port included, for v4 in the format ":" and for v6 in the // format "[]:". With-port and without-port formats are seamlessly supported concurrently. func (p xForwardedForClientIPParser) GetRealClientIP(h http.Header) (net.IP, error) { var ipStr string if realIP := h.Get(p.header); realIP != "" { ipStr = realIP } else { return nil, nil } // Each successive proxy may append itself, comma separated, to the end of the X-Forwarded-for header. // Select only the first IP listed, as it is the client IP recorded by the first proxy. if commaIndex := strings.IndexRune(ipStr, ','); commaIndex != -1 { ipStr = ipStr[:commaIndex] } ipStr = strings.TrimSpace(ipStr) if ipHost, _, err := net.SplitHostPort(ipStr); err == nil { ipStr = ipHost } ip := net.ParseIP(ipStr) if ip == nil { return nil, fmt.Errorf("unable to parse ip (%s) from %s header", ipStr, http.CanonicalHeaderKey(p.header)) } return ip, nil } // getRemoteIP obtains the IP of the low-level connected network host func getRemoteIP(req *http.Request) (net.IP, error) { if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil { return nil, fmt.Errorf("unable to get ip and port from http.RemoteAddr (%s)", req.RemoteAddr) } else if ip := net.ParseIP(ipStr); ip != nil { return ip, nil } else { return nil, fmt.Errorf("unable to parse ip (%s)", ipStr) } } // getClientString obtains the human readable string of the remote IP and optionally the real client IP if available func getClientString(p realClientIPParser, req *http.Request, full bool) (s string) { var realClientIPStr string if p != nil { if realClientIP, err := p.GetRealClientIP(req.Header); err != nil { logger.Printf("Unable to get real client IP: %v", err) } else if realClientIP != nil { realClientIPStr = realClientIP.String() } } var remoteIPStr string if remoteIP, err := getRemoteIP(req); err == nil { remoteIPStr = remoteIP.String() } else { // Should not happen, if it does, likely a bug. logger.Printf("Unable to get remote IP(?!?!): %v", err) } if !full && realClientIPStr != "" { return realClientIPStr } if full && realClientIPStr != "" { return fmt.Sprintf("%s (%s)", remoteIPStr, realClientIPStr) } return remoteIPStr }