1package middleware
2
3import (
4 "net"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8
9 "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
10 "github.com/stretchr/testify/assert"
11)
12
13func TestGetClientIP(t *testing.T) {
14 // Parse trusted proxies for tests
15 trustedNets := httphelper.ParseTrustedProxies([]string{"127.0.0.1", "10.0.0.0/8"})
16 noTrustedNets := []*net.IPNet{}
17 // Include a Cloudflare IP in trusted proxies to test Cf-Connecting-Ip
18 trustedWithCF := httphelper.ParseTrustedProxies([]string{"127.0.0.1", "10.0.0.0/8", "173.245.48.0/20"})
19
20 tests := []struct {
21 name string
22 remoteAddr string
23 xff string
24 xRealIP string
25 cfIP string
26 trustedNets []*net.IPNet
27 wantIP string
28 }{
29 {
30 name: "from RemoteAddr with port",
31 remoteAddr: "192.168.1.100:12345",
32 trustedNets: noTrustedNets,
33 wantIP: "192.168.1.100",
34 },
35 {
36 name: "from RemoteAddr without port",
37 remoteAddr: "192.168.1.100",
38 trustedNets: noTrustedNets,
39 wantIP: "192.168.1.100",
40 },
41 {
42 name: "XFF ignored when no trusted proxies",
43 remoteAddr: "192.168.1.100:12345",
44 xff: "203.0.113.50",
45 trustedNets: noTrustedNets,
46 wantIP: "192.168.1.100",
47 },
48 {
49 name: "XFF ignored when remote is not trusted",
50 remoteAddr: "192.168.1.100:12345",
51 xff: "203.0.113.50",
52 trustedNets: trustedNets, // trusts 127.0.0.1 and 10.0.0.0/8
53 wantIP: "192.168.1.100",
54 },
55 {
56 name: "XFF honored from trusted proxy (single IP)",
57 remoteAddr: "127.0.0.1:12345",
58 xff: "203.0.113.50",
59 trustedNets: trustedNets,
60 wantIP: "203.0.113.50",
61 },
62 {
63 name: "XFF honored from trusted proxy (multiple IPs)",
64 remoteAddr: "127.0.0.1:12345",
65 xff: "203.0.113.50, 10.0.0.1",
66 trustedNets: trustedNets,
67 wantIP: "203.0.113.50",
68 },
69 {
70 name: "XFF spoof chain returns first untrusted from right",
71 remoteAddr: "127.0.0.1:12345",
72 xff: "198.51.100.123, 203.0.113.50, 10.0.0.9",
73 trustedNets: trustedNets,
74 wantIP: "203.0.113.50",
75 },
76 {
77 name: "XFF honored from trusted CIDR",
78 remoteAddr: "10.1.2.3:12345",
79 xff: "203.0.113.50",
80 trustedNets: trustedNets,
81 wantIP: "203.0.113.50",
82 },
83 {
84 name: "X-Real-IP honored from trusted proxy",
85 remoteAddr: "127.0.0.1:12345",
86 xRealIP: "203.0.113.50",
87 trustedNets: trustedNets,
88 wantIP: "203.0.113.50",
89 },
90 {
91 name: "X-Real-IP ignored when not trusted",
92 remoteAddr: "192.168.1.100:12345",
93 xRealIP: "203.0.113.50",
94 trustedNets: trustedNets,
95 wantIP: "192.168.1.100",
96 },
97 {
98 name: "XFF takes precedence over X-Real-IP",
99 remoteAddr: "127.0.0.1:12345",
100 xff: "203.0.113.50",
101 xRealIP: "203.0.113.60",
102 trustedNets: trustedNets,
103 wantIP: "203.0.113.50",
104 },
105 {
106 name: "Cf-Connecting-Ip honored from Cloudflare IP",
107 remoteAddr: "173.245.48.1:12345",
108 cfIP: "203.0.113.99",
109 trustedNets: trustedWithCF,
110 wantIP: "203.0.113.99",
111 },
112 {
113 name: "Cf-Connecting-Ip ignored from non-Cloudflare trusted proxy",
114 remoteAddr: "127.0.0.1:12345",
115 cfIP: "203.0.113.99",
116 xff: "203.0.113.50",
117 trustedNets: trustedNets,
118 wantIP: "203.0.113.50",
119 },
120 {
121 name: "Cf-Connecting-Ip ignored from untrusted IP",
122 remoteAddr: "192.168.1.100:12345",
123 cfIP: "203.0.113.99",
124 trustedNets: trustedNets,
125 wantIP: "192.168.1.100",
126 },
127 {
128 name: "IPv6 RemoteAddr",
129 remoteAddr: "[::1]:12345",
130 trustedNets: noTrustedNets,
131 wantIP: "::1",
132 },
133 }
134
135 for _, tt := range tests {
136 t.Run(tt.name, func(t *testing.T) {
137 req := httptest.NewRequest(http.MethodGet, "/", nil)
138 req.RemoteAddr = tt.remoteAddr
139 if tt.xff != "" {
140 req.Header.Set("X-Forwarded-For", tt.xff)
141 }
142 if tt.xRealIP != "" {
143 req.Header.Set("X-Real-IP", tt.xRealIP)
144 }
145 if tt.cfIP != "" {
146 req.Header.Set("Cf-Connecting-Ip", tt.cfIP)
147 }
148
149 ip := GetClientIP(req, tt.trustedNets)
150 assert.Equal(t, tt.wantIP, ip)
151 })
152 }
153}
154
155func TestParseTrustedProxies(t *testing.T) {
156 tests := []struct {
157 name string
158 proxies []string
159 testIP string
160 want bool
161 }{
162 {
163 name: "empty list",
164 proxies: []string{},
165 testIP: "192.168.1.1",
166 want: false,
167 },
168 {
169 name: "single IP match",
170 proxies: []string{"192.168.1.1"},
171 testIP: "192.168.1.1",
172 want: true,
173 },
174 {
175 name: "single IP no match",
176 proxies: []string{"192.168.1.1"},
177 testIP: "192.168.1.2",
178 want: false,
179 },
180 {
181 name: "CIDR match",
182 proxies: []string{"10.0.0.0/8"},
183 testIP: "10.1.2.3",
184 want: true,
185 },
186 {
187 name: "CIDR no match",
188 proxies: []string{"10.0.0.0/8"},
189 testIP: "192.168.1.1",
190 want: false,
191 },
192 {
193 name: "mixed IP and CIDR",
194 proxies: []string{"127.0.0.1", "10.0.0.0/8", "172.16.0.0/12"},
195 testIP: "172.20.1.1",
196 want: true,
197 },
198 {
199 name: "invalid entries ignored",
200 proxies: []string{"not-an-ip", "10.0.0.0/8"},
201 testIP: "10.1.2.3",
202 want: true,
203 },
204 {
205 name: "IPv6 single IP",
206 proxies: []string{"::1"},
207 testIP: "::1",
208 want: true,
209 },
210 {
211 name: "IPv6 CIDR",
212 proxies: []string{"fd00::/8"},
213 testIP: "fd12:3456::1",
214 want: true,
215 },
216 }
217
218 for _, tt := range tests {
219 t.Run(tt.name, func(t *testing.T) {
220 nets := httphelper.ParseTrustedProxies(tt.proxies)
221 got := httphelper.IsTrustedProxy(tt.testIP, nets)
222 assert.Equal(t, tt.want, got)
223 })
224 }
225}
226
227func TestIsTrustedProxy(t *testing.T) {
228 tests := []struct {
229 name string
230 ip string
231 trustedNets []*net.IPNet
232 want bool
233 }{
234 {
235 name: "empty trusted nets",
236 ip: "192.168.1.1",
237 trustedNets: []*net.IPNet{},
238 want: false,
239 },
240 {
241 name: "nil trusted nets",
242 ip: "192.168.1.1",
243 trustedNets: nil,
244 want: false,
245 },
246 {
247 name: "invalid IP",
248 ip: "not-an-ip",
249 trustedNets: httphelper.ParseTrustedProxies([]string{"192.168.1.0/24"}),
250 want: false,
251 },
252 {
253 name: "empty IP",
254 ip: "",
255 trustedNets: httphelper.ParseTrustedProxies([]string{"192.168.1.0/24"}),
256 want: false,
257 },
258 }
259
260 for _, tt := range tests {
261 t.Run(tt.name, func(t *testing.T) {
262 got := httphelper.IsTrustedProxy(tt.ip, tt.trustedNets)
263 assert.Equal(t, tt.want, got)
264 })
265 }
266}