check / check (push) Successful in 4m26s
The vendored files are fetched from sneak/prompts commit dd4027b. This repository's own entries are kept after the canonical content: /bin in .dockerignore, the Go lines of .gitignore and [*.go] in .editorconfig; the test-support deny list has no entries of its own. The lint phase moves to golangci-lint v2.14.0. The build stage now takes the version from git describe on the .git the build context carries, unless VERSION is passed, and fails when .git is present but no version comes out. The test phase drops -count=1, which the policy says it does not need, and keeps its tmpfs build cache. One test calls Header.Get with X-Real-IP, as canonicalheader asks. Model: opus-5-5
162 lines
5.0 KiB
Go
162 lines
5.0 KiB
Go
package proxy_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
// trustLocalhost trusts the address every test connects from, and a
|
|
// network for proxies in front of it.
|
|
trustLocalhost = localhost + "/32,10.0.0.0/8"
|
|
// appHost is the host every test asks for.
|
|
appHost = "app.example"
|
|
// client is the client's address, as a proxy names it.
|
|
client = "203.0.113.9"
|
|
// forwardedFor is the header that lists the client and its proxies.
|
|
forwardedFor = "X-Forwarded-For"
|
|
// secure is the scheme a client reached traefik with.
|
|
secure = "https"
|
|
)
|
|
|
|
// appHeaders is what the app tells about the headers it received.
|
|
type appHeaders struct {
|
|
Host string `json:"host"`
|
|
ForwardedFor string `json:"forwardedFor"`
|
|
ForwardedHost string `json:"forwardedHost"`
|
|
ForwardedProto string `json:"forwardedProto"`
|
|
RealIP string `json:"realIp"`
|
|
}
|
|
|
|
// clientAddressCase is a request and what smallwebwaf makes of it.
|
|
type clientAddressCase struct {
|
|
name string
|
|
env map[string]string
|
|
header http.Header
|
|
wantClient string
|
|
wantApp appHeaders
|
|
}
|
|
|
|
func TestClientAddressAndForwardedHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range clientAddressCases() {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, line := requestWithHeaders(t, tc.env, tc.header)
|
|
|
|
tc.wantApp.Host = appHost
|
|
if got != tc.wantApp {
|
|
t.Errorf("app received %+v, want %+v", got, tc.wantApp)
|
|
}
|
|
|
|
if line.ClientIP != tc.wantClient || line.PeerIP != localhost {
|
|
t.Errorf("log line has client_ip %q and peer_ip %q, want %q and %q",
|
|
line.ClientIP, line.PeerIP, tc.wantClient, localhost)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// clientAddressCases are the requests TestClientAddressAndForwardedHeaders
|
|
// sends, from 127.0.0.1, which the default trusted proxies leave out.
|
|
func clientAddressCases() []clientAddressCase {
|
|
trusted := map[string]string{trustedProxies: trustLocalhost}
|
|
forged := http.Header{
|
|
forwardedFor: {client},
|
|
"X-Forwarded-Host": {"forged.example"},
|
|
"X-Forwarded-Proto": {secure},
|
|
"X-Real-Ip": {client},
|
|
}
|
|
replaced := appHeaders{
|
|
ForwardedFor: localhost, ForwardedHost: appHost, ForwardedProto: "http",
|
|
}
|
|
|
|
return []clientAddressCase{{
|
|
name: "a peer outside the trusted proxies is the client, " +
|
|
"and its forwarded headers are replaced",
|
|
header: forged, wantClient: localhost, wantApp: replaced,
|
|
}, {
|
|
name: "set but empty, the trusted proxies trust nothing",
|
|
env: map[string]string{trustedProxies: ""},
|
|
header: forged, wantClient: localhost, wantApp: replaced,
|
|
}, {
|
|
name: "behind a trusted peer, the client is the first address " +
|
|
"outside the trusted proxies from the right",
|
|
env: trusted,
|
|
header: http.Header{
|
|
forwardedFor: {"198.51.100.7, " + client + ", 10.0.0.2"},
|
|
"X-Forwarded-Host": {appHost},
|
|
"X-Forwarded-Proto": {secure},
|
|
"X-Real-Ip": {client},
|
|
},
|
|
wantClient: client,
|
|
wantApp: appHeaders{
|
|
ForwardedFor: "198.51.100.7, " + client + ", 10.0.0.2, " + localhost,
|
|
ForwardedHost: appHost, ForwardedProto: secure, RealIP: client,
|
|
},
|
|
}, {
|
|
name: "when every address is a trusted proxy, the leftmost is the client",
|
|
env: trusted,
|
|
header: http.Header{forwardedFor: {"10.0.0.5, 10.0.0.2"}},
|
|
wantClient: "10.0.0.5",
|
|
wantApp: appHeaders{ForwardedFor: "10.0.0.5, 10.0.0.2, " + localhost},
|
|
}, {
|
|
name: "with no header, a trusted peer is the client",
|
|
env: trusted,
|
|
wantClient: localhost,
|
|
wantApp: appHeaders{ForwardedFor: localhost},
|
|
}, {
|
|
name: "an entry that is not an address ends the reading",
|
|
env: trusted,
|
|
header: http.Header{forwardedFor: {client + ", unknown, 10.0.0.2"}},
|
|
wantClient: "10.0.0.2",
|
|
wantApp: appHeaders{
|
|
ForwardedFor: client + ", unknown, 10.0.0.2, " + localhost,
|
|
},
|
|
}, {
|
|
name: "several header lines are read as one list",
|
|
env: trusted,
|
|
header: http.Header{forwardedFor: {"2001:db8::7", "10.0.0.2"}},
|
|
wantClient: "2001:db8::7",
|
|
wantApp: appHeaders{ForwardedFor: "2001:db8::7, 10.0.0.2, " + localhost},
|
|
}}
|
|
}
|
|
|
|
// requestWithHeaders sends a request for appHost with header through
|
|
// smallwebwaf, with the settings in env, and returns the headers the app
|
|
// received and the request's log line.
|
|
func requestWithHeaders(
|
|
t *testing.T, env map[string]string, header http.Header,
|
|
) (appHeaders, logLine) {
|
|
t.Helper()
|
|
|
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(appHeaders{
|
|
Host: r.Host,
|
|
ForwardedFor: r.Header.Get(forwardedFor),
|
|
ForwardedHost: r.Header.Get("X-Forwarded-Host"),
|
|
ForwardedProto: r.Header.Get("X-Forwarded-Proto"),
|
|
RealIP: r.Header.Get("X-Real-IP"),
|
|
})
|
|
})
|
|
addr, out := startProxy(t, app.URL, env)
|
|
|
|
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
|
|
req.Host = appHost
|
|
req.Header = header.Clone()
|
|
|
|
answered := do(t, req)
|
|
|
|
var got appHeaders
|
|
|
|
err := json.Unmarshal(answered.body, &got)
|
|
if err != nil {
|
|
t.Fatalf("decode the app's answer %q: %v", answered.body, err)
|
|
}
|
|
|
|
return got, out.requestLine(t)
|
|
}
|