mirror of
https://github.com/gin-gonic/gin.git
synced 2026-04-30 07:38:15 +08:00
fix(context): add tests and documentation for ClientIP with X-Forwarded-For
Add comprehensive tests for ClientIP behavior with X-Forwarded-For header handling, including edge cases for trusted proxies. Issue: #4572
This commit is contained in:
parent
d3ffc99852
commit
55df62e4f6
@ -2107,6 +2107,31 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
|
|
||||||
c.engine.TrustedPlatform = ""
|
c.engine.TrustedPlatform = ""
|
||||||
|
|
||||||
|
// Test non-standard X-Forwarded-For header content (issue #4572)
|
||||||
|
// IPv6 with brackets only
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", " [::1], 20.20.20.20, 30.30.30.30")
|
||||||
|
assert.Equal(t, "::1", c.ClientIP())
|
||||||
|
|
||||||
|
// IPv6 with brackets and port
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "[2001:db8::1]:8080, 30.30.30.30")
|
||||||
|
assert.Equal(t, "2001:db8::1", c.ClientIP())
|
||||||
|
|
||||||
|
// IPv4 with port
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30"})
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20:8888, 30.30.30.30")
|
||||||
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
|
// Mixed: IPv6 with brackets, IPv4 with port
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30"})
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "[::1]:9999, 20.20.20.20:8080, 30.30.30.30")
|
||||||
|
assert.Equal(t, "::1", c.ClientIP())
|
||||||
|
|
||||||
// no port
|
// no port
|
||||||
c.Request.RemoteAddr = "50.50.50.50"
|
c.Request.RemoteAddr = "50.50.50.50"
|
||||||
assert.Empty(t, c.ClientIP())
|
assert.Empty(t, c.ClientIP())
|
||||||
|
|||||||
20
gin.go
20
gin.go
@ -486,6 +486,7 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
items := strings.Split(header, ",")
|
items := strings.Split(header, ",")
|
||||||
for i := len(items) - 1; i >= 0; i-- {
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
ipStr := strings.TrimSpace(items[i])
|
ipStr := strings.TrimSpace(items[i])
|
||||||
|
ipStr = normalizeIP(ipStr)
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
break
|
break
|
||||||
@ -500,6 +501,25 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeIP strips brackets and port from an IP address string.
|
||||||
|
// This handles non-standard X-Forwarded-For header content like:
|
||||||
|
// - IPv6 with brackets: [::1] or [2001:db8::1]
|
||||||
|
// - IPv4 with port: 192.168.1.1:8080
|
||||||
|
// - IPv6 with brackets and port: [::1]:8080
|
||||||
|
func normalizeIP(ipStr string) string {
|
||||||
|
// Try to split host and port (handles "ip:port" and "[ipv6]:port" formats)
|
||||||
|
if host, _, err := net.SplitHostPort(ipStr); err == nil {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
// If SplitHostPort fails, it might be an IPv6 with brackets only
|
||||||
|
// e.g., "[2001:db8::1]" - strip the brackets
|
||||||
|
if len(ipStr) > 1 && ipStr[0] == '[' && ipStr[len(ipStr)-1] == ']' {
|
||||||
|
return ipStr[1 : len(ipStr)-1]
|
||||||
|
}
|
||||||
|
// Return as-is for plain IPs (IPv4 or IPv6 without brackets)
|
||||||
|
return ipStr
|
||||||
|
}
|
||||||
|
|
||||||
// updateRouteTree do update to the route tree recursively
|
// updateRouteTree do update to the route tree recursively
|
||||||
func updateRouteTree(n *node) {
|
func updateRouteTree(n *node) {
|
||||||
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
|
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
|
||||||
|
|||||||
35
gin_test.go
35
gin_test.go
@ -1156,3 +1156,38 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
|
|||||||
assert.Equal(t, "ok", w.Body.String())
|
assert.Equal(t, "ok", w.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNormalizeIP tests the normalizeIP function for handling non-standard
|
||||||
|
// X-Forwarded-For header content (issue #4572)
|
||||||
|
func TestNormalizeIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Plain IPv4
|
||||||
|
{name: "plain IPv4", input: "192.168.1.1", expected: "192.168.1.1"},
|
||||||
|
// IPv4 with port
|
||||||
|
{name: "IPv4 with port", input: "192.168.1.1:8080", expected: "192.168.1.1"},
|
||||||
|
// Plain IPv6
|
||||||
|
{name: "plain IPv6", input: "::1", expected: "::1"},
|
||||||
|
{name: "plain IPv6 full", input: "2001:db8::1", expected: "2001:db8::1"},
|
||||||
|
// IPv6 with brackets only
|
||||||
|
{name: "IPv6 with brackets only", input: "[::1]", expected: "::1"},
|
||||||
|
{name: "IPv6 with brackets full", input: "[2001:db8::1]", expected: "2001:db8::1"},
|
||||||
|
// IPv6 with brackets and port
|
||||||
|
{name: "IPv6 with brackets and port", input: "[::1]:8080", expected: "::1"},
|
||||||
|
{name: "IPv6 with brackets and port full", input: "[2001:db8::1]:8080", expected: "2001:db8::1"},
|
||||||
|
// Empty string
|
||||||
|
{name: "empty string", input: "", expected: ""},
|
||||||
|
// Invalid IP (return as-is for net.ParseIP to handle)
|
||||||
|
{name: "invalid IP", input: "not-an-ip", expected: "not-an-ip"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := normalizeIP(tt.input)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user