// Copyright (c) 2026 Probo Inc . // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package clientip_test import ( "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "go.probo.inc/probo/pkg/server/api/clientip" ) func TestExtract(t *testing.T) { t.Parallel() tests := []struct { name string remoteAddr string headers map[string]string want string }{ { name: "remote addr only", remoteAddr: "192.168.1.1:12345", want: "192.168.1.1", }, { name: "remote addr without port", remoteAddr: "192.168.1.1", want: "192.168.1.1", }, { name: "x-forwarded-for single ip", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"X-Forwarded-For": "203.0.113.50"}, want: "203.0.113.50", }, { name: "x-forwarded-for chain takes rightmost", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"X-Forwarded-For": "203.0.113.50, 70.41.3.18, 150.172.238.178"}, want: "150.172.238.178", }, { name: "x-forwarded-for spoofed prefix", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"X-Forwarded-For": "1.2.3.4, 203.0.113.50"}, want: "203.0.113.50", }, { name: "x-forwarded-for with port", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"X-Forwarded-For": "203.0.113.50:8080"}, want: "203.0.113.50", }, { name: "forwarded header simple", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": "for=198.51.100.17"}, want: "198.51.100.17", }, { name: "forwarded header quoted", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": `for="198.51.100.17"`}, want: "198.51.100.17", }, { name: "forwarded header ipv6 bracketed", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": `for="[2001:db8::1]"`}, want: "2001:db8::1", }, { name: "forwarded header with port", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": `for="198.51.100.17:4711"`}, want: "198.51.100.17", }, { name: "forwarded header chain takes rightmost", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": "for=198.51.100.17, for=70.41.3.18"}, want: "70.41.3.18", }, { name: "forwarded takes precedence over x-forwarded-for", remoteAddr: "10.0.0.1:1234", headers: map[string]string{ "Forwarded": "for=198.51.100.17", "X-Forwarded-For": "203.0.113.50", }, want: "198.51.100.17", }, { name: "forwarded with extra directives", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": "for=198.51.100.17;proto=https;by=203.0.113.60"}, want: "198.51.100.17", }, { name: "unparseable forwarded falls back to remote addr", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": "for=unknown"}, want: "10.0.0.1", }, { name: "unparseable x-forwarded-for falls back to remote addr", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"X-Forwarded-For": "unknown"}, want: "10.0.0.1", }, { name: "unparseable forwarded falls through to x-forwarded-for", remoteAddr: "10.0.0.1:1234", headers: map[string]string{ "Forwarded": "for=unknown", "X-Forwarded-For": "203.0.113.50", }, want: "203.0.113.50", }, } for _, tt := range tests { t.Run( tt.name, func(t *testing.T) { t.Parallel() r := httptest.NewRequest("GET", "/", nil) r.RemoteAddr = tt.remoteAddr for k, v := range tt.headers { r.Header.Set(k, v) } assert.Equal(t, tt.want, clientip.Extract(r)) }, ) } }