// 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()) }