Read every setting through viper and rename the settings (closes #211)
check / check (push) Failing after 3m27s

Every setting of discriminator 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. Discriminator'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_.
Every place a name appeared changed: the code, the viper keys, tests,
documents, examples, the README's settings table and the image files.

Model: deepseek-v4-pro
This commit is contained in:
sneak committed 2026-11-07 06:41:11 +00:00
1 parent beb503b233
commit 68da92f414
89 files changed
+2027 -1916

No files matched your search

+4 -4
View File
@@ -10,16 +10,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
@@ -68,7 +68,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 -5
View File
@@ -65,9 +65,9 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
rq.answer(http.StatusBadGateway)
default:
// discriminator cannot be connected to, does not answer at all, or does not answer
// within SWRP_TIMEOUT_MS. A request that keeps discriminator busy past
// SWRP_TIMEOUT_MS, like any request while discriminator 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 discriminator busy past
// SWW_RPROXY_TIMEOUT_MS, like any request while discriminator is overloaded, gets
// SWW_RPROXY_DEFAULT_DISPOSITION here. deny closes this, at the cost of answering 503
// whenever discriminator is slow. Making such requests hard to send is
// https://git.eeqj.de/sneak/smallwebwaf/issues/182.
rq.h.processLog.Warn("discriminator gave no answer", "error", err.Error())
@@ -84,7 +84,7 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
}
// ask sends discriminator 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) (*discriminatorAnswer, error) {
ctx, cancel := context.WithTimeout(ctx, rq.h.config.Timeout)
@@ -228,7 +228,7 @@ func (h *handler) sendReport(ctx context.Context, report *discriminator.Report)
}
// postReport posts report to discriminator, and waits for its answer for up to
// SWRP_TIMEOUT_MS.
// SWW_RPROXY_TIMEOUT_MS.
func (h *handler) postReport(ctx context.Context, report *discriminator.Report) error {
body, err := json.Marshal(report)
if err != nil {
+3 -7
View File
@@ -33,13 +33,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(viperFrom(map[string]string{
"SWW_RPROXY_WAF_BACKEND": nowhere,
}))
if err != nil {
t.Fatalf("settings: %v", err)
}
+13 -13
View File
@@ -50,10 +50,10 @@ type request struct {
start time.Time
// disposition is what is done with the request: discriminator'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 discriminator 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 discriminator's X-Client-ASN and X-Client-Country, for the
@@ -118,10 +118,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 discriminator is asked first; passToApp then refuses it.
func (rq *request) readFirstPartOfBody() bool {
if rq.body == nil {
@@ -190,7 +190,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
// discriminator's.
func (rq *request) rewrite(pr *httputil.ProxyRequest) {
@@ -325,7 +325,7 @@ func (rq *request) send(status int, body []byte) {
// 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.
func (rq *request) prepareOwnAnswer(deadline time.Time) {
rq.stopReadingBody(deadline)
@@ -419,7 +419,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 {
@@ -440,7 +440,7 @@ func (rq *request) stopReadingBody(deadline time.Time) {
_ = rq.rc.SetReadDeadline(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()
@@ -454,7 +454,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
@@ -500,7 +500,7 @@ func (rq *request) gotFirstResponseByte() {
}
// 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
@@ -524,7 +524,7 @@ func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {
}
}
// 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()
@@ -539,7 +539,7 @@ func (rq *request) responseTimedOut() {
// answer before the visitor has sent its whole body, as with a 404 it sends
// without reading it. The time limits then run on: until the transport's read of
// the body returns, Go's server cannot send the answer, and
// SWRP_MAX_REQUEST_TIME_MS is what ends that read for a visitor that stops
// SWW_RPROXY_MAX_REQUEST_TIME_MS is what ends that read for a visitor that stops
// sending.
func (rq *request) responseReceived() {
if rq.body != nil && !rq.body.received.Load() {
@@ -549,7 +549,7 @@ func (rq *request) responseReceived() {
rq.stopTimers()
}
// setResponseDeadline sets SWRP_MAX_RESPONSE_TIME_MS on the connection to the
// setResponseDeadline sets SWW_RPROXY_MAX_RESPONSE_TIME_MS on the connection to the
// visitor: the app's answer must reach the visitor within it of the end of the
// request.
func (rq *request) setResponseDeadline() {
+6 -5
View File
@@ -1,9 +1,10 @@
// Package rproxy is the reverse proxy in front of one app that asks discriminator
// about each request. It passes a request discriminator allows to the app and the
// app's answer back, within the size and time limits. Any other answer from
// discriminator goes to the visitor as discriminator gave it. SWRP_DEFAULT_DISPOSITION
// discriminator goes to the visitor as discriminator gave it.
// SWW_RPROXY_DEFAULT_DISPOSITION
// decides only when discriminator 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 discriminator as the visitor
// sent it. It keeps nothing between requests and never touches disk. "rproxy" in
// SPEC.md says what it does.
@@ -71,7 +72,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 {
@@ -103,7 +104,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()
@@ -212,7 +213,7 @@ 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. discriminator answers a path under adminPrefix itself, so such a
// request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
// 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
+22 -13
View File
@@ -18,6 +18,8 @@ import (
"testing"
"time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
@@ -43,13 +45,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.
@@ -345,6 +347,17 @@ func startServer(
return addr, out, server
}
// viperFrom returns a viper Set with each of settings, as the viper
// config.NewViper returns one bound to a process's environment.
func viperFrom(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
// startServerLogging is startServer with rproxy's request lines going to
// requestLog and its process lines to processLog.
func startServerLogging(
@@ -354,15 +367,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(viperFrom(settings))
if err != nil {
t.Fatalf("settings %v: %v", settings, err)
}