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
103 lines
3.0 KiB
Go
103 lines
3.0 KiB
Go
// Package proxy passes each request to the app and the app's answer back,
|
|
// unchanged, within the size and time limits, and writes one request log
|
|
// line for each request.
|
|
package proxy
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
)
|
|
|
|
// The request line and headers a client may send, and how long a
|
|
// kept-open client connection may wait for its next request, are fixed
|
|
// rather than settings. The idle time is longer than the 90 seconds after
|
|
// which traefik closes a connection it is not using, so traefik never
|
|
// sends a request on a connection smallwebwaf is closing.
|
|
const (
|
|
requestHeaderMaxBytes = 32 << 10
|
|
clientIdleTimeout = 120 * time.Second
|
|
)
|
|
|
|
// How smallwebwaf keeps connections to the app open between requests.
|
|
const (
|
|
appIdleConns = 100
|
|
appIdleConnTimeout = 90 * time.Second
|
|
)
|
|
|
|
// Params are what New needs.
|
|
type Params struct {
|
|
Config *config.Config
|
|
// RequestLog receives one JSON line per request.
|
|
RequestLog io.Writer
|
|
// ProcessLog receives the process's own messages.
|
|
ProcessLog *slog.Logger
|
|
}
|
|
|
|
// New returns the server smallwebwaf runs: each request it reads passes
|
|
// through the proxy. Go's server itself refuses headers over 32 KiB, with
|
|
// 431, closes a connection idle for 120 seconds, and applies
|
|
// SWWAF_CLIENT_REQUEST_TIMEOUT while the headers arrive; the proxy
|
|
// applies the timeouts and size limits from then on.
|
|
func New(params Params) *http.Server {
|
|
errorLog := slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn)
|
|
|
|
return &http.Server{
|
|
Addr: params.Config.ListenAddr,
|
|
Handler: &handler{
|
|
config: params.Config,
|
|
requestLog: params.RequestLog,
|
|
processLog: params.ProcessLog,
|
|
errorLog: errorLog,
|
|
transport: newTransport(),
|
|
},
|
|
ReadHeaderTimeout: params.Config.ClientRequestTimeout,
|
|
IdleTimeout: clientIdleTimeout,
|
|
MaxHeaderBytes: requestHeaderMaxBytes,
|
|
ErrorLog: errorLog,
|
|
}
|
|
}
|
|
|
|
// handler is the proxy. It holds what every request shares; what belongs
|
|
// to one request is in a request.
|
|
type handler struct {
|
|
config *config.Config
|
|
requestLog io.Writer
|
|
processLog *slog.Logger
|
|
errorLog *log.Logger
|
|
transport http.RoundTripper
|
|
}
|
|
|
|
// newTransport returns what carries requests to the app. It never goes
|
|
// through a proxy named in the environment, and leaves the app's answers
|
|
// compressed or not as the app sent them.
|
|
func newTransport() *http.Transport {
|
|
return &http.Transport{
|
|
MaxIdleConns: appIdleConns,
|
|
MaxIdleConnsPerHost: appIdleConns,
|
|
IdleConnTimeout: appIdleConnTimeout,
|
|
DisableCompression: true,
|
|
}
|
|
}
|
|
|
|
// ServeHTTP handles one request: it works out the client, runs the
|
|
// checks, passes the request to the app and the answer back within the
|
|
// limits, and writes the request's log line.
|
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
rq := h.newRequest(w, r)
|
|
defer rq.finish()
|
|
|
|
refused := rq.check()
|
|
if refused != nil {
|
|
rq.answer(*refused)
|
|
|
|
return
|
|
}
|
|
|
|
rq.forward(r.Context())
|
|
}
|