check / check (push) Waiting to run
Each request log line now has the fields "Request log" in SPEC.md lists whose features are built: instance (SWWAF_INSTANCE_NAME), scheme, request_id (a trusted proxy's X-Request-ID or a new one, sent on to the app), forwarded_for, client_group, content_type, content_length, the headers SWWAF_LOG_REQUEST_HEADERS names, has_authorization, has_cookie, websocket, response_content_type, cache_control, location, counts and the timings. Authorization, Cookie and Set-Cookie values are never logged. An entry of SWWAF_LOG_REQUEST_HEADERS that is not a header name, or is Host or Transfer-Encoding, stops the start. Deviation: counts has request totals only. Deviation: SWWAF_INSTANCE_NAME is on request lines only. Model: opus-5-5
166 lines
5.2 KiB
Go
166 lines
5.2 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,
|
|
// and forwardedProto the one that gives the scheme the client used.
|
|
forwardedFor = "X-Forwarded-For"
|
|
forwardedProto = "X-Forwarded-Proto"
|
|
// secure is the scheme a client reached traefik with, and plain the
|
|
// one smallwebwaf serves.
|
|
secure = "https"
|
|
plain = "http"
|
|
)
|
|
|
|
// 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"},
|
|
forwardedProto: {secure},
|
|
"X-Real-Ip": {client},
|
|
}
|
|
replaced := appHeaders{
|
|
ForwardedFor: localhost, ForwardedHost: appHost, ForwardedProto: plain,
|
|
}
|
|
|
|
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},
|
|
forwardedProto: {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(forwardedProto),
|
|
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)
|
|
}
|