Pass-through proxy with timeouts, size limits and a request log (closes #13)
check / check (push) Successful in 2m9s
check / check (push) Successful in 2m9s
The repo's first code, with the layout the prompts policies ask for: Makefile, script/ entrypoints, a Dockerfile whose lint and test phases gate the build, the Gitea workflow, the canonical dotfiles and REPO_POLICIES.md. smallwebwaf passes each request to the app through httputil.ReverseProxy within the four timeouts and two size limits, works out the client's address behind trusted proxies, and writes one JSON line per request. The tests run against real local servers. SPEC.md now says what Go's HTTP server does before smallwebwaf sees a request; make fmt only rewraps EVALUATION.md. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user