first commit
This commit is contained in:
147
internal/clientip/resolver.go
Normal file
147
internal/clientip/resolver.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package clientip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
TrustProxyHeaders bool
|
||||
TrustedProxies []netip.Prefix
|
||||
}
|
||||
|
||||
func (r Resolver) FromRequest(req *http.Request) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if r.TrustsProxyHeaders(req) {
|
||||
if addr, ok := firstForwarded(headerValues(req.Header, "Forwarded")); ok {
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
if addr, ok := firstForwardedFor(headerValues(req.Header, "X-Forwarded-For")); ok {
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
if addr, ok := firstAddress(headerValues(req.Header, "X-Real-IP")); ok {
|
||||
return addr.String()
|
||||
}
|
||||
}
|
||||
|
||||
if addr, ok := parseAddress(req.RemoteAddr); ok {
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r Resolver) TrustsProxyHeaders(req *http.Request) bool {
|
||||
if !r.TrustProxyHeaders || req == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(r.TrustedProxies) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
remoteAddr, ok := parseAddress(req.RemoteAddr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, trustedProxy := range r.TrustedProxies {
|
||||
if trustedProxy.Contains(remoteAddr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func headerValues(header http.Header, name string) []string {
|
||||
values := header.Values(name)
|
||||
if len(values) > 0 {
|
||||
return values
|
||||
}
|
||||
|
||||
for key, values := range header {
|
||||
if strings.EqualFold(key, name) {
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstForwarded(values []string) (netip.Addr, bool) {
|
||||
for _, value := range values {
|
||||
for _, element := range strings.Split(value, ",") {
|
||||
for _, pair := range strings.Split(element, ";") {
|
||||
key, rawValue, ok := strings.Cut(pair, "=")
|
||||
if !ok || !strings.EqualFold(strings.TrimSpace(key), "for") {
|
||||
continue
|
||||
}
|
||||
|
||||
if addr, ok := parseAddress(rawValue); ok {
|
||||
return addr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func firstForwardedFor(values []string) (netip.Addr, bool) {
|
||||
for _, value := range values {
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
if addr, ok := parseAddress(part); ok {
|
||||
return addr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func firstAddress(values []string) (netip.Addr, bool) {
|
||||
for _, value := range values {
|
||||
if addr, ok := parseAddress(value); ok {
|
||||
return addr, true
|
||||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func parseAddress(value string) (netip.Addr, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.Trim(value, `"`)
|
||||
|
||||
if addr, ok := parseBareAddress(value); ok {
|
||||
return addr, true
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(value)
|
||||
if err != nil {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
return parseBareAddress(host)
|
||||
}
|
||||
|
||||
func parseBareAddress(value string) (netip.Addr, bool) {
|
||||
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
|
||||
value = strings.TrimPrefix(strings.TrimSuffix(value, "]"), "[")
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(value)
|
||||
if err != nil {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
return addr.Unmap(), true
|
||||
}
|
||||
137
internal/clientip/resolver_test.go
Normal file
137
internal/clientip/resolver_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package clientip
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolverUsesRemoteAddressByDefault(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{"X-Forwarded-For": {"203.0.113.10"}},
|
||||
RemoteAddr: "198.51.100.20:54123",
|
||||
}
|
||||
|
||||
got := (Resolver{}).FromRequest(req)
|
||||
want := "198.51.100.20"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverCanTrustForwardedFor(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{
|
||||
"X-Forwarded-For": {"203.0.113.10, 198.51.100.20"},
|
||||
},
|
||||
RemoteAddr: "192.0.2.30:54123",
|
||||
}
|
||||
|
||||
got := (Resolver{TrustProxyHeaders: true}).FromRequest(req)
|
||||
want := "203.0.113.10"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverCanTrustRFCForwardedHeader(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{
|
||||
"Forwarded": {`for="203.0.113.10";proto=https;host=ip.example.com`},
|
||||
},
|
||||
RemoteAddr: "192.0.2.30:54123",
|
||||
}
|
||||
|
||||
got := (Resolver{TrustProxyHeaders: true}).FromRequest(req)
|
||||
want := "203.0.113.10"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverOnlyTrustsConfiguredProxyCIDRs(t *testing.T) {
|
||||
trustedProxy := netip.MustParsePrefix("127.0.0.1/32")
|
||||
resolver := Resolver{
|
||||
TrustProxyHeaders: true,
|
||||
TrustedProxies: []netip.Prefix{trustedProxy},
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Header: http.Header{"X-Forwarded-For": {"203.0.113.10"}},
|
||||
RemoteAddr: "198.51.100.20:54123",
|
||||
}
|
||||
|
||||
got := resolver.FromRequest(req)
|
||||
want := "198.51.100.20"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverTrustsForwardedHeaderFromConfiguredProxyCIDR(t *testing.T) {
|
||||
trustedProxy := netip.MustParsePrefix("127.0.0.1/32")
|
||||
resolver := Resolver{
|
||||
TrustProxyHeaders: true,
|
||||
TrustedProxies: []netip.Prefix{trustedProxy},
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Header: http.Header{"X-Forwarded-For": {"203.0.113.10"}},
|
||||
RemoteAddr: "127.0.0.1:54123",
|
||||
}
|
||||
|
||||
got := resolver.FromRequest(req)
|
||||
want := "203.0.113.10"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverFallsBackToRealIPHeader(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{"X-Real-IP": {"203.0.113.10"}},
|
||||
RemoteAddr: "192.0.2.30:54123",
|
||||
}
|
||||
|
||||
got := (Resolver{TrustProxyHeaders: true}).FromRequest(req)
|
||||
want := "203.0.113.10"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverHandlesIPv6RemoteAddress(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{},
|
||||
RemoteAddr: "[2001:db8::1]:54123",
|
||||
}
|
||||
|
||||
got := (Resolver{}).FromRequest(req)
|
||||
want := "2001:db8::1"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverSkipsInvalidForwardedForEntries(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{
|
||||
"X-Forwarded-For": {"unknown, 203.0.113.10"},
|
||||
},
|
||||
RemoteAddr: "192.0.2.30:54123",
|
||||
}
|
||||
|
||||
got := (Resolver{TrustProxyHeaders: true}).FromRequest(req)
|
||||
want := "203.0.113.10"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user