mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2026-10-18 13:18:26 +02:00
Move RealClientIP code to IP packages
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
|
||||
)
|
||||
|
||||
func GetRealClientIPParser(headerKey string) (ipapi.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 `<client>` portion specified in the above document.
|
||||
// Additionally, is capable of parsing IPs with the port included, for v4 in the format "<ip>:<port>" and for v6 in the
|
||||
// format "[<ip>]:<port>". 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 ipapi.RealClientIPParser, req *http.Request, full bool) (s string) {
|
||||
var realClientIPStr string
|
||||
if p != nil {
|
||||
if realClientIP, err := p.GetRealClientIP(req.Header); err == nil && realClientIP != nil {
|
||||
realClientIPStr = realClientIP.String()
|
||||
}
|
||||
}
|
||||
|
||||
var remoteIPStr string
|
||||
if remoteIP, err := getRemoteIP(req); err == nil {
|
||||
remoteIPStr = remoteIP.String()
|
||||
}
|
||||
|
||||
if !full && realClientIPStr != "" {
|
||||
return realClientIPStr
|
||||
}
|
||||
if full && realClientIPStr != "" {
|
||||
return fmt.Sprintf("%s (%s)", remoteIPStr, realClientIPStr)
|
||||
}
|
||||
return remoteIPStr
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRealClientIPParser(t *testing.T) {
|
||||
forwardedForType := reflect.TypeOf((*xForwardedForClientIPParser)(nil))
|
||||
|
||||
tests := []struct {
|
||||
header string
|
||||
errString string
|
||||
parserType reflect.Type
|
||||
}{
|
||||
{"X-Forwarded-For", "", forwardedForType},
|
||||
{"X-REAL-IP", "", forwardedForType},
|
||||
{"x-proxyuser-ip", "", forwardedForType},
|
||||
{"", "the http header key () is either invalid or unsupported", nil},
|
||||
{"Forwarded", "the http header key (Forwarded) is either invalid or unsupported", nil},
|
||||
{"2#* @##$$:kd", "the http header key (2#* @##$$:kd) is either invalid or unsupported", nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
p, err := GetRealClientIPParser(test.header)
|
||||
|
||||
if test.errString == "" {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, test.errString, err.Error())
|
||||
}
|
||||
|
||||
if test.parserType == nil {
|
||||
assert.Nil(t, p)
|
||||
} else {
|
||||
assert.NotNil(t, p)
|
||||
assert.Equal(t, test.parserType, reflect.TypeOf(p))
|
||||
}
|
||||
|
||||
if xp, ok := p.(*xForwardedForClientIPParser); ok {
|
||||
assert.Equal(t, http.CanonicalHeaderKey(test.header), xp.header)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXForwardedForClientIPParser(t *testing.T) {
|
||||
p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
|
||||
|
||||
tests := []struct {
|
||||
headerValue string
|
||||
errString string
|
||||
expectedIP net.IP
|
||||
}{
|
||||
{"", "", nil},
|
||||
{"1.2.3.4", "", net.ParseIP("1.2.3.4")},
|
||||
{"10::23", "", net.ParseIP("10::23")},
|
||||
{"::1", "", net.ParseIP("::1")},
|
||||
{"[::1]:1234", "", net.ParseIP("::1")},
|
||||
{"10.0.10.11:1234", "", net.ParseIP("10.0.10.11")},
|
||||
{"192.168.10.50, 10.0.0.1, 1.2.3.4", "", net.ParseIP("192.168.10.50")},
|
||||
{"nil", "unable to parse ip (nil) from X-Forwarded-For header", nil},
|
||||
{"10000.10000.10000.10000", "unable to parse ip (10000.10000.10000.10000) from X-Forwarded-For header", nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
h := http.Header{}
|
||||
h.Add("X-Forwarded-For", test.headerValue)
|
||||
|
||||
ip, err := p.GetRealClientIP(h)
|
||||
|
||||
if test.errString == "" {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, test.errString, err.Error())
|
||||
}
|
||||
|
||||
if test.expectedIP == nil {
|
||||
assert.Nil(t, ip)
|
||||
} else {
|
||||
assert.NotNil(t, ip)
|
||||
assert.Equal(t, test.expectedIP, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXForwardedForClientIPParserIgnoresOthers(t *testing.T) {
|
||||
p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
|
||||
|
||||
h := http.Header{}
|
||||
expectedIPString := "192.168.10.50"
|
||||
h.Add("X-Real-IP", "10.0.0.1")
|
||||
h.Add("X-ProxyUser-IP", "10.0.0.1")
|
||||
h.Add("X-Forwarded-For", expectedIPString)
|
||||
ip, err := p.GetRealClientIP(h)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ip)
|
||||
assert.Equal(t, ip, net.ParseIP(expectedIPString))
|
||||
}
|
||||
|
||||
func TestGetRemoteIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
remoteAddr string
|
||||
errString string
|
||||
expectedIP net.IP
|
||||
}{
|
||||
{"", "unable to get ip and port from http.RemoteAddr ()", nil},
|
||||
{"nil", "unable to get ip and port from http.RemoteAddr (nil)", nil},
|
||||
{"235.28.129.186", "unable to get ip and port from http.RemoteAddr (235.28.129.186)", nil},
|
||||
{"90::45", "unable to get ip and port from http.RemoteAddr (90::45)", nil},
|
||||
{"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},
|
||||
{"10000.10000.10000.10000:8080", "unable to parse ip (10000.10000.10000.10000)", nil},
|
||||
{"[::1]:48290", "", net.ParseIP("::1")},
|
||||
{"10.254.244.165:62750", "", net.ParseIP("10.254.244.165")},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
req := &http.Request{RemoteAddr: test.remoteAddr}
|
||||
|
||||
ip, err := getRemoteIP(req)
|
||||
|
||||
if test.errString == "" {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, test.errString, err.Error())
|
||||
}
|
||||
|
||||
if test.expectedIP == nil {
|
||||
assert.Nil(t, ip)
|
||||
} else {
|
||||
assert.NotNil(t, ip)
|
||||
assert.Equal(t, test.expectedIP, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientString(t *testing.T) {
|
||||
p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
|
||||
|
||||
tests := []struct {
|
||||
parser ipapi.RealClientIPParser
|
||||
remoteAddr string
|
||||
headerValue string
|
||||
expectedClient string
|
||||
expectedClientFull string
|
||||
}{
|
||||
// Should fail quietly, only printing warnings to the log
|
||||
{nil, "", "", "", ""},
|
||||
{p, "127.0.0.1:11950", "", "127.0.0.1", "127.0.0.1"},
|
||||
{p, "[::1]:28660", "99.103.56.12", "99.103.56.12", "::1 (99.103.56.12)"},
|
||||
{nil, "10.254.244.165:62750", "", "10.254.244.165", "10.254.244.165"},
|
||||
// Parser is nil, the contents of X-Forwarded-For should be ignored in all cases.
|
||||
{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"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
h := http.Header{}
|
||||
h.Add("X-Forwarded-For", test.headerValue)
|
||||
req := &http.Request{
|
||||
Header: h,
|
||||
RemoteAddr: test.remoteAddr,
|
||||
}
|
||||
|
||||
client := GetClientString(test.parser, req, false)
|
||||
assert.Equal(t, test.expectedClient, client)
|
||||
|
||||
clientFull := GetClientString(test.parser, req, true)
|
||||
assert.Equal(t, test.expectedClientFull, clientFull)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user