check / check (push) Successful in 1m46s
The request line and headers are now refused above 32 KiB exactly: Go's server reads 4 KiB past MaxHeaderBytes, so that is set to 28 KiB, and the tests try exactly 32 KiB and one byte more. SWWAF_UPSTREAM_URL now needs a host, and a given port must be from 1 to 65535. make build and make run call script/build and script/run. 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())
|
|
}
|