Files
BountyBoard/internal/http/proxy_test.go
T
etalon 976f5238f8 feat: scaffold app skeleton with config, ULID, health endpoints, compose stack (phase 1)
- internal .env loader (real env wins) + strictly validated config struct
- ULID package: 48-bit ms timestamp + 80-bit crypto randomness, sortable
- HTTP server with recovery/request-id/trusted-proxy/access-log middleware
- /healthz, /readyz (pluggable checkers), /metricsz (counter registry)
- multi-stage Dockerfile (alpine, non-root) + compose with healthchecks,
  app bound to 127.0.0.1:8787, mongo unpublished, graceful 15s shutdown

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:10:30 +02:00

155 lines
4.3 KiB
Go

package httpx
import (
"net/http"
"net/http/httptest"
"net/netip"
"testing"
)
func prefixes(t *testing.T, cidrs ...string) []netip.Prefix {
t.Helper()
var out []netip.Prefix
for _, c := range cidrs {
p, err := netip.ParsePrefix(c)
if err != nil {
t.Fatal(err)
}
out = append(out, p)
}
return out
}
func TestResolveClient(t *testing.T) {
trusted := "172.16.0.0/12,127.0.0.0/8"
tests := []struct {
name string
remoteAddr string
cidrs string
hdr map[string][]string
wantIP string
wantProto string
wantHost string // empty = request Host
}{
{
name: "no proxies configured: headers ignored",
remoteAddr: "203.0.113.7:4444",
cidrs: "",
hdr: map[string][]string{"X-Forwarded-For": {"198.51.100.1"}, "X-Forwarded-Proto": {"https"}},
wantIP: "203.0.113.7",
wantProto: "http",
},
{
name: "untrusted peer spoofing headers: ignored",
remoteAddr: "203.0.113.7:4444",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"198.51.100.1"}, "X-Forwarded-Proto": {"https"}, "X-Forwarded-Host": {"evil.example"}},
wantIP: "203.0.113.7",
wantProto: "http",
},
{
name: "trusted peer, single hop",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"198.51.100.1"}, "X-Forwarded-Proto": {"https"}, "X-Forwarded-Host": {"bounty.example.com"}},
wantIP: "198.51.100.1",
wantProto: "https",
wantHost: "bounty.example.com",
},
{
name: "client-spoofed XFF prefix is skipped (rightmost untrusted wins)",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"6.6.6.6, 198.51.100.1"}},
wantIP: "198.51.100.1",
wantProto: "http",
},
{
name: "chain of trusted proxies collapses to real client",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"198.51.100.1, 172.20.0.5, 172.20.0.9"}},
wantIP: "198.51.100.1",
wantProto: "http",
},
{
name: "all-trusted chain falls back to leftmost",
remoteAddr: "127.0.0.1:9999",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"172.20.0.5"}},
wantIP: "172.20.0.5",
wantProto: "http",
},
{
name: "trusted peer without forwarded headers",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: nil,
wantIP: "172.18.0.2",
wantProto: "http",
},
{
name: "malformed XFF entry stops the walk",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"garbage, 198.51.100.1"}},
wantIP: "198.51.100.1",
wantProto: "http",
},
{
name: "multiple XFF headers are concatenated in order",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"198.51.100.1", "172.20.0.5"}},
wantIP: "198.51.100.1",
wantProto: "http",
},
{
name: "invalid forwarded proto ignored",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"198.51.100.1"}, "X-Forwarded-Proto": {"gopher"}},
wantIP: "198.51.100.1",
wantProto: "http",
},
{
name: "ipv6 client through trusted proxy",
remoteAddr: "172.18.0.2:1234",
cidrs: trusted,
hdr: map[string][]string{"X-Forwarded-For": {"2001:db8::1"}},
wantIP: "2001:db8::1",
wantProto: "http",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "http://app.local/", nil)
r.RemoteAddr = tt.remoteAddr
for k, vs := range tt.hdr {
for _, v := range vs {
r.Header.Add(k, v)
}
}
var cidrs []netip.Prefix
if tt.cidrs != "" {
cidrs = prefixes(t, "172.16.0.0/12", "127.0.0.0/8")
}
got := resolveClient(r, cidrs, "http")
if got.IP.String() != tt.wantIP {
t.Errorf("IP = %s, want %s", got.IP, tt.wantIP)
}
if got.Proto != tt.wantProto {
t.Errorf("Proto = %s, want %s", got.Proto, tt.wantProto)
}
wantHost := tt.wantHost
if wantHost == "" {
wantHost = "app.local"
}
if got.Host != wantHost {
t.Errorf("Host = %s, want %s", got.Host, wantHost)
}
})
}
}