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
102 lines
3.0 KiB
Go
102 lines
3.0 KiB
Go
package proxy
|
|
|
|
import (
|
|
"net/http"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// peerAddress is the address of the request's TCP peer, normally traefik.
|
|
func peerAddress(r *http.Request) netip.Addr {
|
|
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return netip.Addr{}
|
|
}
|
|
|
|
return addrPort.Addr().Unmap()
|
|
}
|
|
|
|
// clientAddress works out who the client is. A peer outside the trusted
|
|
// proxies is the client, and what it says in X-Forwarded-For is ignored.
|
|
// For a peer inside them, X-Forwarded-For is read from the right, and the
|
|
// first address outside them is the client; if every address in it is
|
|
// inside, the leftmost is, and with no header, the peer. An entry that is
|
|
// not an address ends the reading, since nothing to its left can be
|
|
// believed.
|
|
func clientAddress(
|
|
peer netip.Addr, forwardedFor []string, trusted []netip.Prefix,
|
|
) netip.Addr {
|
|
client := peer
|
|
if !isInside(peer, trusted) {
|
|
return client
|
|
}
|
|
|
|
entries := strings.Split(strings.Join(forwardedFor, ","), ",")
|
|
for _, entry := range slices.Backward(entries) {
|
|
addr, err := netip.ParseAddr(strings.TrimSpace(entry))
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
client = addr.Unmap()
|
|
if !isInside(client, trusted) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// isInside reports whether addr is in one of the netblocks.
|
|
func isInside(addr netip.Addr, netblocks []netip.Prefix) bool {
|
|
return slices.ContainsFunc(netblocks, func(netblock netip.Prefix) bool {
|
|
return netblock.Contains(addr)
|
|
})
|
|
}
|
|
|
|
// setForwardedHeaders sets the headers in which the app learns about the
|
|
// client, so that it sees what it would see from traefik directly. A
|
|
// trusted proxy's forwarded headers pass on, with the proxy's own address
|
|
// added to X-Forwarded-For. Those of any other peer are its own claims and
|
|
// are replaced: X-Forwarded-For names the peer, X-Forwarded-Host the host
|
|
// it asked for, and X-Forwarded-Proto plain http, which is how it reached
|
|
// smallwebwaf.
|
|
func setForwardedHeaders(in, out *http.Request, peer netip.Addr, trusted bool) {
|
|
forwardedFor := peer.String()
|
|
|
|
if trusted {
|
|
// ReverseProxy removes these from out before Rewrite.
|
|
for _, name := range []string{"Forwarded", "X-Forwarded-Host", "X-Forwarded-Proto"} {
|
|
values, ok := in.Header[name]
|
|
if ok {
|
|
out.Header[name] = values
|
|
}
|
|
}
|
|
|
|
prior := in.Header.Values("X-Forwarded-For")
|
|
if len(prior) > 0 {
|
|
forwardedFor = strings.Join(prior, ", ") + ", " + forwardedFor
|
|
}
|
|
|
|
out.Header.Set("X-Forwarded-For", forwardedFor)
|
|
|
|
return
|
|
}
|
|
|
|
// ReverseProxy has removed Forwarded and the three set below; these
|
|
// are the other headers in which traefik tells the app about the
|
|
// client and its request.
|
|
for _, name := range []string{
|
|
"X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri",
|
|
"X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert",
|
|
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
|
|
} {
|
|
out.Header.Del(name)
|
|
}
|
|
|
|
out.Header.Set("X-Forwarded-For", forwardedFor)
|
|
out.Header.Set("X-Forwarded-Host", in.Host)
|
|
out.Header.Set("X-Forwarded-Proto", "http")
|
|
}
|