check / check (push) Successful in 1m29s
Milestone 1, the repo's first code. smallwebwaf passes each request to the app and the answer back unchanged, streaming bodies and WebSocket upgrades, within four timeouts (client and app, request and response) and two size limits, and writes one JSON line per request to stdout. Every setting has an SWWAF_ name and a default, and an invalid value stops the start. The repo gets the standard layout: script/ entrypoints, make targets that call them, a Dockerfile that runs the checks, and the Gitea workflow. Disclosure: SPEC.md changed. Go's server reads the request line and headers before smallwebwaf sees the request, so slow headers are closed without an answer, and neither slow nor oversized headers get a log line. Disclosure: standard library only. 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)
|
|
}
|