check / check (push) Successful in 1m29s
Milestone 1, the repo's first code. smallwebwaf passes each request to the app and the answer back unchanged, streaming bodies and WebSocket upgrades, within four timeouts (client and app, request and response) and two size limits, and writes one JSON line per request to stdout. Every setting has an SWWAF_ name and a default, and an invalid value stops the start. The repo gets the standard layout: script/ entrypoints, make targets that call them, a Dockerfile that runs the checks, and the Gitea workflow. Disclosure: SPEC.md changed. Go's server reads the request line and headers before smallwebwaf sees the request, so slow headers are closed without an answer, and neither slow nor oversized headers get a log line. Disclosure: standard library only. Model: opus-5-5
106 lines
3.1 KiB
Go
106 lines
3.1 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 limit on the request line and headers is
|
|
// 32 KiB, but Go's server reads 4 KiB past its MaxHeaderBytes before it
|
|
// refuses, so MaxHeaderBytes is set 4 KiB lower. 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 - 4<<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())
|
|
}
|