Read every setting through viper and rename the settings (closes #211)
check / check (push) Waiting to run

Every setting of waf and rproxy is read through viper, in place of the
os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and
AllowEmptyEnv on, so a setting that is set but empty reads as set. A
setting may also be given as a file, the _FILE form, which rproxy's
settings now have too; rproxy still writes nothing to disk.

The settings are renamed: waf's take the prefix SWW_WAF_, rproxy's take
SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's
settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_.

Model: deepseek-v4-pro (first version); opus-5-5 (rework)
This commit is contained in:
sneak committed 2026-11-11 10:15:12 +00:00
1 parent b93bad5842
commit c516e34145
122 files changed
+2288 -2170

No files matched your search

+4 -4
View File
@@ -11,16 +11,16 @@ import (
)
// errResponseTooLarge ends an app's response body that is longer than
// SWRP_MAX_RESPONSE_SIZE.
// SWW_RPROXY_MAX_RESPONSE_SIZE.
var errResponseTooLarge = errors.New(
"the response body is over SWRP_MAX_RESPONSE_SIZE")
"the response body is over SWW_RPROXY_MAX_RESPONSE_SIZE")
// requestBody is the visitor's request body. rproxy first reads up to the first
// 64 KiB of it into held, and then, on its way to the app, the transport reads
// it on a goroutine of its own.
type requestBody struct {
// body is the visitor's body, ending in an *http.MaxBytesError past
// SWRP_MAX_REQUEST_SIZE.
// SWW_RPROXY_MAX_REQUEST_SIZE.
body io.ReadCloser
rq *request
// held is the part read before the request goes to the app, which Read gives
@@ -84,7 +84,7 @@ func (b *requestBody) Close() error {
// responseBody is the app's response body on its way to the visitor.
type responseBody struct {
// body is the app's body, ending in an *http.MaxBytesError past
// SWRP_MAX_RESPONSE_SIZE.
// SWW_RPROXY_MAX_RESPONSE_SIZE.
body io.ReadCloser
rq *request
}
+5 -8
View File
@@ -17,6 +17,7 @@ import (
"time"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rproxy"
"sneak.berlin/go/smallwebwaf/internal/waf"
@@ -34,13 +35,9 @@ func announcedLength(size int) string {
func TestServerHasTheFixedLimits(t *testing.T) {
t.Parallel()
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
if name == "SWRP_WAF_BACKEND" {
return nowhere, true
}
return "", false
})
cfg, err := config.RProxyFromEnvironment(configtest.Viper(map[string]string{
"SWW_RPROXY_WAF_BACKEND": nowhere,
}))
if err != nil {
t.Fatalf("settings: %v", err)
}
@@ -701,7 +698,7 @@ func TestAVisitorGetsAnAnswerWhenTheAppAnswersAsTheTimeRunsOut(t *testing.T) {
}
// visitAsTheTimeRunsOut sends requests to an app that answers about as
// SWRP_MAX_RESPONSE_TIME_MS runs out, and checks that each is answered with the
// SWW_RPROXY_MAX_RESPONSE_TIME_MS runs out, and checks that each is answered with the
// app's 200 or a 504.
func visitAsTheTimeRunsOut(t *testing.T) {
t.Helper()
+18 -18
View File
@@ -34,14 +34,14 @@ const plainText = "text/plain; charset=utf-8"
// minSendTime is the least time to send an answer rproxy gives itself, or the
// status and headers of the app's answer. A shorter time can run out before the
// first byte is written, on a busy host or as SWRP_MAX_RESPONSE_TIME_MS runs out,
// first byte is written, on a busy host or as SWW_RPROXY_MAX_RESPONSE_TIME_MS runs out,
// and the visitor then gets a closed connection and no answer.
const minSendTime = 10 * time.Second
// errResponseTooLate refuses an app's answer that arrives once
// SWRP_MAX_RESPONSE_TIME_MS has run out.
// SWW_RPROXY_MAX_RESPONSE_TIME_MS has run out.
var errResponseTooLate = errors.New(
"the answer arrived after SWRP_MAX_RESPONSE_TIME_MS ran out")
"the answer arrived after SWW_RPROXY_MAX_RESPONSE_TIME_MS ran out")
// request is one request on its way through rproxy, from the moment its headers
// have been read to its log line and report.
@@ -61,10 +61,10 @@ type request struct {
start time.Time
// disposition is what is done with the request: waf's valid disposition,
// or SWRP_DEFAULT_DISPOSITION. It is "" when neither gave one.
// or SWW_RPROXY_DEFAULT_DISPOSITION. It is "" when neither gave one.
disposition string
// answered is true when waf gave disposition, which makes a report due,
// and byDefault when SWRP_DEFAULT_DISPOSITION did.
// and byDefault when SWW_RPROXY_DEFAULT_DISPOSITION did.
answered bool
byDefault bool
// asn and country are waf's X-Client-ASN and X-Client-Country, for the
@@ -127,10 +127,10 @@ func (rq *request) answerHealth() {
}
// readFirstPartOfBody reads up to the first 64 KiB of the body into memory,
// within SWRP_MAX_REQUEST_TIME_MS, and reports whether the request goes on. A
// within SWW_RPROXY_MAX_REQUEST_TIME_MS, and reports whether the request goes on. A
// visitor too slow to send it is answered 408. For a malformed chunk, or a
// visitor that went away or stopped its sending side, the connection is closed
// without an answer. A body over SWRP_MAX_REQUEST_SIZE goes on with the part
// without an answer. A body over SWW_RPROXY_MAX_REQUEST_SIZE goes on with the part
// read, so that waf is asked first; passToApp then refuses it.
func (rq *request) readFirstPartOfBody() bool {
if rq.body == nil {
@@ -198,7 +198,7 @@ func (rq *request) forward(ctx context.Context) {
}
// rewrite makes the request the app receives: the visitor's request, unchanged,
// sent to SWRP_UPSTREAM_URL, with the forwarded headers and the request's id set,
// sent to SWW_RPROXY_UPSTREAM_URL, with the forwarded headers and the request's id set,
// without any X-Client-ASN or X-Client-Country the visitor sent, and with
// waf's.
func (rq *request) rewrite(pr *httputil.ProxyRequest) {
@@ -247,7 +247,7 @@ func (rq *request) modifyResponse(res *http.Response) error {
requestEnd := rq.endRequest()
// An answer that arrives once SWRP_MAX_RESPONSE_TIME_MS has run out is not
// An answer that arrives once SWW_RPROXY_MAX_RESPONSE_TIME_MS has run out is not
// passed on: the visitor is answered 504.
limit := rq.h.config.MaxResponseTime
if limit > 0 && !time.Now().Before(requestEnd.Add(limit)) {
@@ -273,7 +273,7 @@ func (rq *request) modifyResponse(res *http.Response) error {
rq.closeAfterAnswer()
// An answer that arrives before the app has been sent the whole request ends
// the request, and SWRP_MAX_RESPONSE_TIME_MS starts.
// the request, and SWW_RPROXY_MAX_RESPONSE_TIME_MS starts.
rq.startResponseTimer(requestEnd)
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
@@ -361,7 +361,7 @@ func (rq *request) closeAfterAnswer() {
// prepareOwnAnswer readies the connection for an answer rproxy gives itself: the
// reading of a body that has not arrived whole ends at deadline, and the answer
// has SWRP_MAX_RESPONSE_TIME_MS from now to reach the visitor, even when the app
// has SWW_RPROXY_MAX_RESPONSE_TIME_MS from now to reach the visitor, even when the app
// used that time up first, and at least minSendTime.
func (rq *request) prepareOwnAnswer(deadline time.Time) {
rq.stopReadingBody(deadline)
@@ -470,7 +470,7 @@ func (rq *request) bytesEachWay() (int64, int64) {
}
// requestDeadline is when the visitor must have sent its whole request, or zero
// when SWRP_MAX_REQUEST_TIME_MS is off.
// when SWW_RPROXY_MAX_REQUEST_TIME_MS is off.
func (rq *request) requestDeadline() time.Time {
limit := rq.h.config.MaxRequestTime
if limit == 0 {
@@ -511,7 +511,7 @@ func (rq *request) stopPassingOnBody(deadline time.Time) {
rq.stopReadingBody(deadline)
}
// startRequestTimer starts SWRP_MAX_REQUEST_TIME_MS, which runs from the
// startRequestTimer starts SWW_RPROXY_MAX_REQUEST_TIME_MS, which runs from the
// request's headers being read until the app has been sent the whole request.
func (rq *request) startRequestTimer() {
deadline := rq.requestDeadline()
@@ -525,7 +525,7 @@ func (rq *request) startRequestTimer() {
rq.requestTimer = time.AfterFunc(time.Until(deadline), rq.requestTimedOut)
}
// requestTimedOut is called when SWRP_MAX_REQUEST_TIME_MS runs out while the
// requestTimedOut is called when SWW_RPROXY_MAX_REQUEST_TIME_MS runs out while the
// request is still on its way to the app. The answer names the side rproxy was
// waiting on at that moment: 408 when it was waiting for the visitor to send more
// of its body, 504 when it was waiting for the app to be reached or to take what
@@ -552,7 +552,7 @@ func (rq *request) requestTimedOut() {
}
// wroteRequest is called once the app has been sent the whole request:
// SWRP_MAX_REQUEST_TIME_MS ends and SWRP_MAX_RESPONSE_TIME_MS starts.
// SWW_RPROXY_MAX_REQUEST_TIME_MS ends and SWW_RPROXY_MAX_RESPONSE_TIME_MS starts.
func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {
if info.Err != nil {
return // the transport gives up, or tries again
@@ -567,7 +567,7 @@ func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {
rq.startResponseTimer(requestEnd)
}
// startResponseTimer starts SWRP_MAX_RESPONSE_TIME_MS, which runs from
// startResponseTimer starts SWW_RPROXY_MAX_RESPONSE_TIME_MS, which runs from
// requestEnd, unless it has started already.
func (rq *request) startResponseTimer(requestEnd time.Time) {
limit := rq.h.config.MaxResponseTime
@@ -586,7 +586,7 @@ func (rq *request) startResponseTimer(requestEnd time.Time) {
rq.responseTimedOut)
}
// responseTimedOut is called when SWRP_MAX_RESPONSE_TIME_MS runs out before the
// responseTimedOut is called when SWW_RPROXY_MAX_RESPONSE_TIME_MS runs out before the
// app has sent its whole answer.
func (rq *request) responseTimedOut() {
rq.mu.Lock()
@@ -604,7 +604,7 @@ func (rq *request) responseReceived() {
// setResponseDeadline sends the status and headers of the app's answer, with
// at least minSendTime, before ReverseProxy reads the answer's body, and then
// the rest has until SWRP_MAX_RESPONSE_TIME_MS after the end of the request.
// the rest has until SWW_RPROXY_MAX_RESPONSE_TIME_MS after the end of the request.
func (rq *request) setResponseDeadline() {
limit := rq.h.config.MaxResponseTime
if limit == 0 {
+7 -7
View File
@@ -1,11 +1,11 @@
// Package rproxy is the reverse proxy in front of one app that asks waf
// about each request. It passes a request waf allows to the app and the
// app's answer back, within the size and time limits. Any other answer from
// waf goes to the visitor as waf gave it. SWRP_DEFAULT_DISPOSITION
// waf goes to the visitor as waf gave it. SWW_RPROXY_DEFAULT_DISPOSITION
// decides only when waf cannot be connected to, does not answer at all, or
// does not answer within SWRP_TIMEOUT_MS. rproxy itself refuses a request with a
// does not answer within SWW_RPROXY_TIMEOUT_MS. rproxy itself refuses a request with a
// method it does not pass on, or one it cannot put to waf as the visitor
// sent it. It keeps nothing between requests and never touches disk. "rproxy" in
// sent it. It keeps nothing between requests and writes nothing to disk. "rproxy" in
// SPEC.md says what it does.
package rproxy
@@ -73,7 +73,7 @@ type Server struct {
// New returns the server `sww rproxy` runs. Go's server itself refuses
// a request line and headers over requestHeaderMaxBytes, with 431, closes a
// connection idle for idleTimeout, and applies SWRP_MAX_REQUEST_TIME_MS while
// connection idle for idleTimeout, and applies SWW_RPROXY_MAX_REQUEST_TIME_MS while
// the headers arrive. The handler applies the time and size limits from then
// on.
func New(params Params) *Server {
@@ -105,7 +105,7 @@ func New(params Params) *Server {
}
// WaitForReports returns once the reports being sent have gone or failed,
// which SWRP_TIMEOUT_MS bounds. Any report after this call is dropped: that of
// which SWW_RPROXY_TIMEOUT_MS bounds. Any report after this call is dropped: that of
// a WebSocket that closes while rproxy stops, for one.
func (s *Server) WaitForReports() {
s.h.reportsMu.Lock()
@@ -214,8 +214,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// passToApp passes the request on to the app, unless a limit or its path keeps
// it from the app. waf answers a path under ownEndpointPrefix itself, so
// such a request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
// request's own context.
// such a request gets here only when SWW_RPROXY_DEFAULT_DISPOSITION allowed it.
// ctx is the request's own context.
func (rq *request) passToApp(ctx context.Context) {
maxBytes := rq.h.config.MaxRequestSize
+10 -13
View File
@@ -19,6 +19,7 @@ import (
"time"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rproxy"
"sneak.berlin/go/smallwebwaf/internal/waf"
@@ -43,13 +44,13 @@ const (
// The settings the tests set.
const (
timeoutSetting = "SWRP_TIMEOUT_MS"
defaultDisposition = "SWRP_DEFAULT_DISPOSITION"
trustedProxies = "SWRP_TRUSTED_PROXIES"
maxRequestSize = "SWRP_MAX_REQUEST_SIZE"
maxResponseSize = "SWRP_MAX_RESPONSE_SIZE"
maxRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
maxResponseTime = "SWRP_MAX_RESPONSE_TIME_MS"
timeoutSetting = "SWW_RPROXY_TIMEOUT_MS"
defaultDisposition = "SWW_RPROXY_DEFAULT_DISPOSITION"
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
maxRequestSize = "SWW_RPROXY_MAX_REQUEST_SIZE"
maxResponseSize = "SWW_RPROXY_MAX_RESPONSE_SIZE"
maxRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
maxResponseTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
)
// The headers the tests set and look at.
@@ -354,15 +355,11 @@ func startServerLogging(
t.Helper()
settings := map[string]string{
"SWRP_WAF_BACKEND": backend, "SWRP_UPSTREAM_URL": appURL,
"SWW_RPROXY_WAF_BACKEND": backend, "SWW_RPROXY_UPSTREAM_URL": appURL,
}
maps.Copy(settings, env)
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
cfg, err := config.RProxyFromEnvironment(configtest.Viper(settings))
if err != nil {
t.Fatalf("settings %v: %v", settings, err)
}
+5 -5
View File
@@ -65,9 +65,9 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
rq.answer(http.StatusBadGateway)
default:
// waf cannot be connected to, does not answer at all, or does not answer
// within SWRP_TIMEOUT_MS. A request that keeps waf busy past
// SWRP_TIMEOUT_MS, like any request while waf is overloaded, gets
// SWRP_DEFAULT_DISPOSITION here. deny closes this, at the cost of answering 503
// within SWW_RPROXY_TIMEOUT_MS. A request that keeps waf busy past
// SWW_RPROXY_TIMEOUT_MS, like any request while waf is overloaded, gets
// SWW_RPROXY_DEFAULT_DISPOSITION here. deny closes this, at the cost of answering 503
// whenever waf is slow. Making such requests hard to send is
// https://git.eeqj.de/sneak/smallwebwaf/issues/182.
rq.h.processLog.Warn("waf gave no answer", "error", err.Error())
@@ -84,7 +84,7 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
}
// ask sends waf the question, and returns its answer, which must have come
// whole within SWRP_TIMEOUT_MS. It ends when ctx does, as when the visitor goes
// whole within SWW_RPROXY_TIMEOUT_MS. It ends when ctx does, as when the visitor goes
// away.
func (rq *request) ask(ctx context.Context) (*wafAnswer, error) {
ctx, cancel := context.WithTimeout(ctx, rq.h.config.Timeout)
@@ -228,7 +228,7 @@ func (h *handler) sendReport(ctx context.Context, report *waf.Report) {
}
// postReport posts report to waf, and waits for its answer for up to
// SWRP_TIMEOUT_MS.
// SWW_RPROXY_TIMEOUT_MS.
func (h *handler) postReport(ctx context.Context, report *waf.Report) error {
body, err := json.Marshal(report)
if err != nil {