Format Go with gofmt, then golines -m 77 (closes #215)
check / check (push) Waiting to run

script/fmt now runs golines -m 77 after gofmt, repeating golines until
it changes nothing, since one pass can leave a line the next splits.
script/fmt-check fails when either would change a file.
script/bootstrap installs golines v0.13.0 with go install, pinned to
its commit, and checks the installed version with go version -m.

The tree is reformatted. The longer lines this makes are fixed by
hand: nolint directives moved onto their own line above the statement,
one string split, and 20 functions over funlen's 80 lines shortened or
split, RunWAF and limitPercentages among them.

Model: opus-5-5
This commit was merged in pull request #363.
This commit is contained in:
clawbot committed 2026-11-11 17:29:12 +01:00
1 parent 63e58e326b
commit 0b23352767
118 files changed
+9447 -2670

No files matched your search

+4 -2
View File
@@ -203,7 +203,8 @@ func (w *responseWriter) Write(p []byte) (int, error) {
w.status = http.StatusOK
}
n, err := w.ResponseWriter.Write(p) //nolint:gosec // the answer, as it was given
//nolint:gosec // the answer, as it was given
n, err := w.ResponseWriter.Write(p)
w.bytes += int64(n)
return n, err
@@ -218,7 +219,8 @@ func (w *responseWriter) FlushError() error {
// Hijack takes over the connection to the visitor, which ReverseProxy does only
// to pass on the app's 101 Switching Protocols: that is then the status sent.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
conn, buffered, err := http.NewResponseController(w.ResponseWriter).Hijack()
conn, buffered, err := http.NewResponseController(w.ResponseWriter).
Hijack()
if err == nil {
w.status = http.StatusSwitchingProtocols
}
+5 -1
View File
@@ -83,7 +83,11 @@ func isInside(addr netip.Addr, netblocks []netip.Prefix) bool {
// X-Forwarded-For. Those of any other peer are its own claims and are replaced:
// X-Forwarded-For names the peer, X-Forwarded-Host the host it asked for, and
// X-Forwarded-Proto plain http, which is how it reached rproxy.
func setForwardedHeaders(in, out *http.Request, peer netip.Addr, trusted bool) {
func setForwardedHeaders(
in, out *http.Request,
peer netip.Addr,
trusted bool,
) {
forwardedFor := peer.String()
if trusted {
+256 -80
View File
@@ -34,13 +34,15 @@ 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
}
cfg, err := config.RProxyFromEnvironment(
func(name string) (string, bool) {
if name == "SWRP_WAF_BACKEND" {
return nowhere, true
}
return "", false
})
return "", false
},
)
if err != nil {
t.Fatalf("settings: %v", err)
}
@@ -48,14 +50,23 @@ func TestServerHasTheFixedLimits(t *testing.T) {
server := rproxy.New(rproxy.Params{
Config: cfg,
RequestLog: io.Discard,
ProcessLog: requestlog.NewProcessLogger(io.Discard, "test", slog.LevelInfo),
ProcessLog: requestlog.NewProcessLogger(
io.Discard,
"test",
slog.LevelInfo,
),
})
if server.Addr != ":8080" || server.MaxHeaderBytes != 28<<10 ||
server.IdleTimeout != 2*time.Minute || server.ReadHeaderTimeout != time.Minute {
t.Errorf("server listens on %q with header limit %d, idle time %s and "+
"header timeout %s", server.Addr, server.MaxHeaderBytes,
server.IdleTimeout, server.ReadHeaderTimeout)
t.Errorf(
"server listens on %q with header limit %d, idle time %s and "+
"header timeout %s",
server.Addr,
server.MaxHeaderBytes,
server.IdleTimeout,
server.ReadHeaderTimeout,
)
}
}
@@ -76,9 +87,14 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked")
})
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestSize: tc.limit,
})
addr, _ := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
maxRequestSize: tc.limit,
},
)
got := visit(t, newRequest(t, http.MethodPost, addr, "/",
bytes.NewReader(make([]byte, tc.size))))
@@ -87,7 +103,11 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
}
if asked := standIn.question(t); len(asked.body) != tc.asked {
t.Errorf("the question carries %d bytes, want %d", len(asked.body), tc.asked)
t.Errorf(
"the question carries %d bytes, want %d",
len(asked.body),
tc.asked,
)
}
report := standIn.report(t)
@@ -138,13 +158,21 @@ func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
asked.Store(true)
greet(w, r)
})
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestSize: tc.limit,
})
addr, _ := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
maxRequestSize: tc.limit,
},
)
conn := dial(t, addr)
send(t, conn, post+"Transfer-Encoding: chunked\r\n\r\n"+
strconv.FormatInt(int64(tc.size), 16)+"\r\n"+strings.Repeat("x", tc.size)+
strconv.FormatInt(
int64(tc.size),
16,
)+"\r\n"+strings.Repeat("x", tc.size)+
"\r\n0\r\n\r\n")
if status := readStatus(t, conn); status != http.StatusRequestEntityTooLarge {
@@ -203,17 +231,26 @@ func TestAResponseOverItsLimit(t *testing.T) {
text, err := io.ReadAll(conn)
if err != nil || bytes.Contains(text, []byte(large)) {
t.Errorf("read %d bytes (%v), want the answer cut off", len(text), err)
t.Errorf(
"read %d bytes (%v), want the answer cut off",
len(text),
err,
)
}
report := standIn.report(t)
if report.Status != http.StatusOK || report.ResponseBytes > 1<<10 {
t.Errorf("report %+v, want status 200 and at most 1K of the answer", report)
t.Errorf(
"report %+v, want status 200 and at most 1K of the answer",
report,
)
}
})
}
func TestAHeadWhoseAnswerAnnouncesMoreThanTheResponseLimitIsPassedOn(t *testing.T) {
func TestAHeadWhoseAnswerAnnouncesMoreThanTheResponseLimitIsPassedOn(
t *testing.T,
) {
t.Parallel()
const announced = "2048"
@@ -227,8 +264,13 @@ func TestAHeadWhoseAnswerAnnouncesMoreThanTheResponseLimitIsPassedOn(t *testing.
})
got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody))
if got.status != http.StatusOK || got.header.Get(contentLengthHeader) != announced {
t.Errorf("answered %+v, want the app's 200 announcing %s bytes", got, announced)
if got.status != http.StatusOK ||
got.header.Get(contentLengthHeader) != announced {
t.Errorf(
"answered %+v, want the app's 200 announcing %s bytes",
got,
announced,
)
}
}
@@ -243,14 +285,22 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
standIn := startWAF(t, allow)
app := startApp(t, greet)
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
setting: "off",
})
addr, _ := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
setting: "off",
},
)
// A body and an answer that a limit of zero would cut off, as it would the
// next answer, which rproxy gives itself.
wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/",
strings.NewReader("a body"))))
wantGreeting(
t,
visit(t, newRequest(t, http.MethodPost, addr, "/",
strings.NewReader("a body"))),
)
if got := get(t, addr, waf.HealthPath); got.status != http.StatusOK {
t.Errorf("answered %+v, want rproxy's own 200", got)
@@ -273,9 +323,14 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
standIn := startWAF(t, allow)
app := startApp(t, greet)
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "500",
})
addr, out := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
maxRequestTime: "500",
},
)
conn := dial(t, addr)
send(t, conn, post+tc.rest)
@@ -319,7 +374,9 @@ func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
}
}
func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(
t *testing.T,
) {
t.Parallel()
for _, tc := range []struct {
@@ -356,8 +413,11 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
text, err := io.ReadAll(conn)
if err != nil || len(text) != 0 {
t.Errorf("read %q (%v), want the connection closed without an answer",
text, err)
t.Errorf(
"read %q (%v), want the connection closed without an answer",
text,
err,
)
}
line := out.requestLine(t)
@@ -402,7 +462,8 @@ func (w *deadlineRecorder) FlushError() error {
func (w *deadlineRecorder) SetReadDeadline(deadline time.Time) error {
w.readDeadline = deadline
if w.stalled != nil && !deadline.IsZero() && !deadline.After(time.Now()) {
if w.stalled != nil && !deadline.IsZero() &&
!deadline.After(time.Now()) {
_ = w.stalled.CloseWithError(os.ErrDeadlineExceeded)
}
@@ -439,12 +500,22 @@ func TestABodyLeftUnreadIsReadOnlyUntilTheRequestTimeLimit(t *testing.T) {
t.Cleanup(func() { _ = stop.Close() })
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/",
io.MultiReader(bytes.NewReader(make([]byte, questionBodyMaxBytes+1)), stalled))
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/",
io.MultiReader(
bytes.NewReader(make([]byte, questionBodyMaxBytes+1)),
stalled,
),
)
req.ContentLength = announced
req.Header.Set(contentLengthHeader, strconv.Itoa(announced))
w := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder(), stalled: stop}
w := &deadlineRecorder{
ResponseRecorder: httptest.NewRecorder(),
stalled: stop,
}
before := time.Now()
@@ -456,12 +527,17 @@ func TestABodyLeftUnreadIsReadOnlyUntilTheRequestTimeLimit(t *testing.T) {
if w.readDeadline.Before(before.Add(limit)) ||
w.readDeadline.After(time.Now().Add(limit)) {
t.Errorf("the reading of the body ends at %s, want %s from the request",
w.readDeadline, limit)
t.Errorf(
"the reading of the body ends at %s, want %s from the request",
w.readDeadline,
limit,
)
}
}
func TestABodyLeftUnreadIsReadNoMoreOnceTheTimeToSendTheAnswerRunsOut(t *testing.T) {
func TestABodyLeftUnreadIsReadNoMoreOnceTheTimeToSendTheAnswerRunsOut(
t *testing.T,
) {
t.Parallel()
standIn := startWAF(t, allow)
@@ -473,8 +549,15 @@ func TestABodyLeftUnreadIsReadNoMoreOnceTheTimeToSendTheAnswerRunsOut(t *testing
t.Cleanup(func() { _ = stop.Close() })
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/",
io.MultiReader(bytes.NewReader(make([]byte, questionBodyMaxBytes+1)), stalled))
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodPost,
"/",
io.MultiReader(
bytes.NewReader(make([]byte, questionBodyMaxBytes+1)),
stalled,
),
)
req.ContentLength = 2 * questionBodyMaxBytes
req.Header.Set(contentLengthHeader, strconv.Itoa(2*questionBodyMaxBytes))
@@ -496,7 +579,10 @@ func TestABodyLeftUnreadIsReadNoMoreOnceTheTimeToSendTheAnswerRunsOut(t *testing
}
if w.readDeadline.Before(before) || w.readDeadline.After(time.Now()) {
t.Errorf("the reading of the body ends at %s, want now", w.readDeadline)
t.Errorf(
"the reading of the body ends at %s, want now",
w.readDeadline,
)
}
}
@@ -517,12 +603,24 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
standIn := startWAF(t, deny)
app := startApp(t, greet)
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10),
})
_, _, server := startServer(
t,
standIn.URL,
app.URL,
map[string]string{
maxResponseTime: strconv.FormatInt(
limit.Milliseconds(),
10,
),
},
)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, tc.target,
http.NoBody)
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
tc.target,
http.NoBody,
)
w := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
before := time.Now()
@@ -535,14 +633,19 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
if w.writeDeadline.Before(before.Add(limit)) ||
w.writeDeadline.After(time.Now().Add(limit)) {
t.Errorf("the time limit on sending ends at %s, want %s from the answer",
w.writeDeadline, limit)
t.Errorf(
"the time limit on sending ends at %s, want %s from the answer",
w.writeDeadline,
limit,
)
}
})
}
}
func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(
t *testing.T,
) {
t.Parallel()
const least = 10 * time.Second
@@ -553,7 +656,12 @@ func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
maxResponseTime: "300",
})
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
"/",
http.NoBody,
)
w := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
before := time.Now()
@@ -561,8 +669,11 @@ func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
server.Handler.ServeHTTP(w, req)
if w.writeDeadline.Before(before.Add(least)) {
t.Errorf("the time limit on sending ends at %s, want %s from the answer at least",
w.writeDeadline, least)
t.Errorf(
"the time limit on sending ends at %s, want %s from the answer at least",
w.writeDeadline,
least,
)
}
}
@@ -583,7 +694,9 @@ func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
t.Errorf("status %d, want 408", status)
}
if asked := standIn.question(t); len(asked.body) != questionBodyMaxBytes {
if asked := standIn.question(t); len(
asked.body,
) != questionBodyMaxBytes {
t.Errorf("the question carries %d bytes", len(asked.body))
}
@@ -597,7 +710,11 @@ func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
func startAppThatTakesNothing(t *testing.T) string {
t.Helper()
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
listener, err := (&net.ListenConfig{}).Listen(
t.Context(),
"tcp",
localhost+":0",
)
if err != nil {
t.Fatalf("listen: %v", err)
}
@@ -686,7 +803,9 @@ func TestAnAppTooSlowToAnswerIsAnswered504(t *testing.T) {
}
}
func TestAVisitorGetsAnAnswerWhenTheAppAnswersAsTheTimeRunsOut(t *testing.T) {
func TestAVisitorGetsAnAnswerWhenTheAppAnswersAsTheTimeRunsOut(
t *testing.T,
) {
t.Parallel()
// Four visitors at a time, each with an app and rproxy of its own, send more
@@ -785,8 +904,13 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
if res.StatusCode != http.StatusOK || string(body) != start ||
!errors.Is(err, io.ErrUnexpectedEOF) {
t.Errorf("answered %d %q (%v), want the app's 200 %q, then the connection closed",
res.StatusCode, body, err, start)
t.Errorf(
"answered %d %q (%v), want the app's 200 %q, then the connection closed",
res.StatusCode,
body,
err,
start,
)
}
if report := standIn.report(t); report.Status != http.StatusOK {
@@ -827,13 +951,17 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
line := out.requestLine(t)
responseBytes, _ := line["response_bytes"].(float64)
if line["status"] != float64(http.StatusOK) || responseBytes >= bodySize {
if line["status"] != float64(http.StatusOK) ||
responseBytes >= bodySize {
t.Errorf("line %v, want status 200 and the answer cut off", line)
}
report := standIn.report(t)
if report.Status != http.StatusOK || report.ResponseBytes >= bodySize {
t.Errorf("report %+v, want status 200 and the answer cut off", report)
t.Errorf(
"report %+v, want status 200 and the answer cut off",
report,
)
}
}
@@ -853,7 +981,9 @@ func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
}
}
func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing.T) {
func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(
t *testing.T,
) {
t.Parallel()
standIn := startWAF(t, allow)
@@ -937,7 +1067,9 @@ func TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce(t *testing.T) {
wantGreeting(t, readAnswer(t, conn))
}
func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(t *testing.T) {
func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(
t *testing.T,
) {
t.Parallel()
const limit = 2 * time.Second
@@ -961,7 +1093,11 @@ func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(t *testing.
}
if closed := time.Since(start); closed < limit {
t.Errorf("the connection closed after %s, want %s at least", closed, limit)
t.Errorf(
"the connection closed after %s, want %s at least",
closed,
limit,
)
}
}
@@ -992,7 +1128,8 @@ func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOf
rc := http.NewResponseController(w)
_ = rc.EnableFullDuplex()
w.Header().Set(contentLengthHeader, strconv.Itoa(len(greeting)))
w.Header().
Set(contentLengthHeader, strconv.Itoa(len(greeting)))
for _, part := range tc.atOnce {
_, _ = io.WriteString(w, part)
@@ -1010,10 +1147,18 @@ func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOf
})
// The request time limit is far longer than dial's deadline.
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "120000",
maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10),
})
addr, _ := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
maxRequestTime: "120000",
maxResponseTime: strconv.FormatInt(
limit.Milliseconds(),
10,
),
},
)
conn := dial(t, addr)
sendAllButALittle(t, conn)
@@ -1030,7 +1175,12 @@ func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOf
want := strings.Join(tc.atOnce, "")
if string(body) != want || !errors.Is(err, io.ErrUnexpectedEOF) {
t.Errorf("read %q (%v), want %q and the connection closed", body, err, want)
t.Errorf(
"read %q (%v), want %q and the connection closed",
body,
err,
want,
)
}
})
}
@@ -1068,7 +1218,11 @@ func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) {
text, err := io.ReadAll(conn)
if err != nil || len(text) != 0 {
t.Errorf("read %q (%v), want the connection closed without an answer", text, err)
t.Errorf(
"read %q (%v), want the connection closed without an answer",
text,
err,
)
}
if line := out.requestLine(t); line["status"] != 0.0 {
@@ -1076,7 +1230,9 @@ func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) {
}
}
func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing.T) {
func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(
t *testing.T,
) {
t.Parallel()
const answer = "the end of a long answer"
@@ -1094,13 +1250,28 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
maxRequestTime: "2000",
})
got := visit(t, newRequest(t, http.MethodPost, addr, "/", strings.NewReader("a body")))
got := visit(
t,
newRequest(
t,
http.MethodPost,
addr,
"/",
strings.NewReader("a body"),
),
)
if got.status != http.StatusOK || got.body != answer {
t.Errorf("answered %d %q, want the app's whole answer", got.status, got.body)
t.Errorf(
"answered %d %q, want the app's whole answer",
got.status,
got.body,
)
}
}
func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(t *testing.T) {
func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(
t *testing.T,
) {
t.Parallel()
for _, tc := range []struct {
@@ -1121,10 +1292,15 @@ func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(t *testing.T
app := startApp(t, greet)
// The request time limit is longer than the 10 seconds an answer rproxy
// gives itself has to be sent, and than dial's deadline.
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "15000",
maxResponseTime: "300",
})
addr, _ := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
maxRequestTime: "15000",
maxResponseTime: "300",
},
)
// The body stops 100 bytes short, less than Go's server reads of a body
// left unread before it answers.
+102 -31
View File
@@ -54,7 +54,12 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
asked := standIn.question(t)
if asked.method != http.MethodDelete || asked.target != target ||
asked.host != site {
t.Errorf("asked about %s %s at %s", asked.method, asked.target, asked.host)
t.Errorf(
"asked about %s %s at %s",
asked.method,
asked.target,
asked.host,
)
}
if asked.header.Get(requestIDHeader) == "the-visitor's" {
@@ -72,7 +77,11 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
want.Set(waf.PeerHeader, localhost)
if !maps.EqualFunc(asked.header, want, slices.Equal) {
t.Errorf("the question's headers are %v, want %v", asked.header, want)
t.Errorf(
"the question's headers are %v, want %v",
asked.header,
want,
)
}
}
@@ -100,9 +109,14 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
greet(w, r)
})
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: tc.trusted,
})
addr, out := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
trustedProxies: tc.trusted,
},
)
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
req.Header.Set(forwardedForHeader, visitor)
@@ -122,7 +136,12 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
}
if got := (<-forwarded).Get(forwardedForHeader); got != tc.forwarded {
t.Errorf("the app saw %s %q, want %q", forwardedForHeader, got, tc.forwarded)
t.Errorf(
"the app saw %s %q, want %q",
forwardedForHeader,
got,
tc.forwarded,
)
}
})
}
@@ -159,9 +178,14 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
standIn := startWAF(t, allow)
app := startApp(t, greet)
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32,10.0.0.0/8",
})
addr, _ := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
trustedProxies: localhost + "/32,10.0.0.0/8",
},
)
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
req.Header[forwardedForHeader] = tc.header
@@ -200,15 +224,24 @@ func TestFirst64KiBReachWAFAndTheWholeBodyTheApp(t *testing.T) {
body := randomBytes(size)
standIn := startWAF(t, allow)
received := make(chan []byte, 1)
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
whole, _ := io.ReadAll(r.Body)
received <- whole
app := startApp(
t,
func(w http.ResponseWriter, r *http.Request) {
whole, _ := io.ReadAll(r.Body)
received <- whole
greet(w, r)
})
greet(w, r)
},
)
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
req := newRequest(t, http.MethodPost, addr, "/", bytes.NewReader(body))
req := newRequest(
t,
http.MethodPost,
addr,
"/",
bytes.NewReader(body),
)
if !announced {
// Go's client sends a body of unknown length in chunks.
req.ContentLength = -1
@@ -224,7 +257,9 @@ func TestFirst64KiBReachWAFAndTheWholeBodyTheApp(t *testing.T) {
t.Error("the app did not get the whole body")
}
if report := standIn.report(t); report.RequestBytes != int64(size) {
if report := standIn.report(t); report.RequestBytes != int64(
size,
) {
t.Errorf("report %+v, of %d bytes", report, size)
}
})
@@ -240,8 +275,11 @@ func wantQuestionBody(
t.Helper()
if !bytes.Equal(asked.body, want) {
t.Errorf("the question carries %d bytes, want the first %d", len(asked.body),
len(want))
t.Errorf(
"the question carries %d bytes, want the first %d",
len(asked.body),
len(want),
)
}
values := asked.header.Values(waf.ContentLengthHeader)
@@ -256,9 +294,13 @@ func wantQuestionBody(
return
}
if len(values) != 0 || !slices.Equal(asked.transferEncoding, []string{chunked}) {
t.Errorf("announced %v and encoded %v, want none and chunked", values,
asked.transferEncoding)
if len(values) != 0 ||
!slices.Equal(asked.transferEncoding, []string{chunked}) {
t.Errorf(
"announced %v and encoded %v, want none and chunked",
values,
asked.transferEncoding,
)
}
}
@@ -311,7 +353,11 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
conn := dial(t, addr)
send(t, conn, method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing)
send(
t,
conn,
method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing,
)
if status := readStatus(t, conn); status != http.StatusForbidden {
t.Errorf("status %d, want waf's 403", status)
@@ -320,8 +366,13 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
asked := standIn.question(t)
if string(asked.body) != tc.body ||
!slices.Equal(asked.transferEncoding, tc.encoding) {
t.Errorf("the question carries %q, encoded %v, want %q, encoded %v",
asked.body, asked.transferEncoding, tc.body, tc.encoding)
t.Errorf(
"the question carries %q, encoded %v, want %q, encoded %v",
asked.body,
asked.transferEncoding,
tc.body,
tc.encoding,
)
}
})
}
@@ -343,10 +394,15 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
got := visit(t, newRequest(t, method, addr, "/", http.NoBody))
if got.status != http.StatusMethodNotAllowed ||
got.header.Get("Allow") != "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, "+
got.header.Get(
"Allow",
) != "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, "+
"PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK, "+
"MKCALENDAR" {
t.Errorf("answered %+v, want 405 naming the methods passed on", got)
t.Errorf(
"answered %+v, want 405 naming the methods passed on",
got,
)
}
line := out.requestLine(t)
@@ -483,7 +539,12 @@ func TestATrustedPeerWithNoForwardedProtoIsAskedAboutAsHTTP(t *testing.T) {
asked := standIn.question(t)
if got := asked.header.Get(forwardedProtoHeader); got != want {
t.Errorf("the question's %s is %q, want %s", forwardedProtoHeader, got, want)
t.Errorf(
"the question's %s is %q, want %s",
forwardedProtoHeader,
got,
want,
)
}
}
@@ -504,11 +565,21 @@ func TestAQuestionNamesNoZone(t *testing.T) {
standIn := startWAF(t, deny)
app := startApp(t, greet)
_, out, server := startServer(t, standIn.URL, app.URL, map[string]string{
trustedProxies: localhost + "/32",
})
_, out, server := startServer(
t,
standIn.URL,
app.URL,
map[string]string{
trustedProxies: localhost + "/32",
},
)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
"/",
http.NoBody,
)
req.RemoteAddr = tc.remoteAddr
req.Header.Set(forwardedForHeader, tc.forwardedFor)
+69 -32
View File
@@ -96,24 +96,34 @@ type request struct {
// newRequest starts handling r: it notes the time, works out the visitor from
// the TCP peer and X-Forwarded-For, and makes the request's id.
func (h *handler) newRequest(w http.ResponseWriter, r *http.Request) *request {
func (h *handler) newRequest(
w http.ResponseWriter,
r *http.Request,
) *request {
peer := peerAddress(r)
trusted := h.config.TrustedProxies
rq := &request{
h: h,
in: r,
rc: http.NewResponseController(w),
out: &responseWriter{ResponseWriter: w},
id: rand.Text(),
client: clientAddress(peer, r.Header.Values("X-Forwarded-For"), trusted),
h: h,
in: r,
rc: http.NewResponseController(w),
out: &responseWriter{ResponseWriter: w},
id: rand.Text(),
client: clientAddress(
peer,
r.Header.Values("X-Forwarded-For"),
trusted,
),
peer: peer,
peerTrusted: isInside(peer, trusted),
start: time.Now(),
}
if r.Body != http.NoBody {
rq.body = &requestBody{body: limitBody(r.Body, h.config.MaxRequestSize), rq: rq}
rq.body = &requestBody{
body: limitBody(r.Body, h.config.MaxRequestSize),
rq: rq,
}
}
return rq
@@ -138,7 +148,9 @@ func (rq *request) readFirstPartOfBody() bool {
}
_ = rq.rc.SetReadDeadline(rq.requestDeadline())
held, err := io.ReadAll(io.LimitReader(rq.body, waf.QuestionBodyMaxBytes))
held, err := io.ReadAll(
io.LimitReader(rq.body, waf.QuestionBodyMaxBytes),
)
// From here on, the request timer ends a read of the rest of the body, so that
// the answer can name the side rproxy was waiting on.
_ = rq.rc.SetReadDeadline(time.Time{})
@@ -257,7 +269,8 @@ func (rq *request) modifyResponse(res *http.Response) error {
}
maxBytes := rq.h.config.MaxResponseSize
if maxBytes > 0 && res.Body != http.NoBody && res.ContentLength > maxBytes {
if maxBytes > 0 && res.Body != http.NoBody &&
res.ContentLength > maxBytes {
rq.refuse(http.StatusBadGateway)
return errResponseTooLarge
@@ -284,7 +297,11 @@ func (rq *request) modifyResponse(res *http.Response) error {
// answerError is ReverseProxy's ErrorHandler: the request could not be passed to
// the app, or the app's answer cannot be passed on.
func (rq *request) answerError(_ http.ResponseWriter, _ *http.Request, err error) {
func (rq *request) answerError(
_ http.ResponseWriter,
_ *http.Request,
err error,
) {
if rq.upgraded != nil {
// The app switched protocols, but the switch was not passed on, and
// ReverseProxy leaves the app's connection open when it refuses it.
@@ -300,7 +317,11 @@ func (rq *request) answerError(_ http.ResponseWriter, _ *http.Request, err error
panic(http.ErrAbortHandler)
}
rq.h.processLog.Warn("request to the app failed", "error", err.Error())
rq.h.processLog.Warn(
"request to the app failed",
"error",
err.Error(),
)
status = http.StatusBadGateway
}
@@ -411,24 +432,33 @@ func (rq *request) finish(ctx context.Context) {
end := time.Now()
requestBytes, responseBytes := rq.bytesEachWay()
err := requestlog.WriteRProxyLine(rq.h.requestLog, &requestlog.RProxyLine{
Time: requestlog.FormatTime(rq.start),
RequestID: rq.id,
ClientIP: rq.client.String(),
Host: rq.in.Host,
Method: rq.in.Method,
Path: rules.Path(rq.in),
Query: rq.in.URL.RawQuery,
Status: rq.out.status,
RequestBytes: requestBytes,
ResponseBytes: responseBytes,
DurationRequestMS: requestlog.Milliseconds(requestEnd.Sub(rq.start)),
DurationResponseMS: requestlog.Milliseconds(end.Sub(requestEnd)),
Disposition: rq.disposition,
DefaultDisposition: rq.byDefault,
})
err := requestlog.WriteRProxyLine(
rq.h.requestLog,
&requestlog.RProxyLine{
Time: requestlog.FormatTime(rq.start),
RequestID: rq.id,
ClientIP: rq.client.String(),
Host: rq.in.Host,
Method: rq.in.Method,
Path: rules.Path(rq.in),
Query: rq.in.URL.RawQuery,
Status: rq.out.status,
RequestBytes: requestBytes,
ResponseBytes: responseBytes,
DurationRequestMS: requestlog.Milliseconds(
requestEnd.Sub(rq.start),
),
DurationResponseMS: requestlog.Milliseconds(end.Sub(requestEnd)),
Disposition: rq.disposition,
DefaultDisposition: rq.byDefault,
},
)
if err != nil {
rq.h.processLog.Error("writing the request log failed", "error", err.Error())
rq.h.processLog.Error(
"writing the request log failed",
"error",
err.Error(),
)
}
if !rq.answered {
@@ -443,7 +473,9 @@ func (rq *request) finish(ctx context.Context) {
DurationTotalMS: requestlog.Milliseconds(end.Sub(rq.start)),
}
if !rq.upstreamStart.IsZero() {
report.DurationUpstreamTotalMS = requestlog.Milliseconds(end.Sub(rq.upstreamStart))
report.DurationUpstreamTotalMS = requestlog.Milliseconds(
end.Sub(rq.upstreamStart),
)
}
rq.h.sendReport(ctx, report)
@@ -522,7 +554,10 @@ func (rq *request) startRequestTimer() {
rq.mu.Lock()
defer rq.mu.Unlock()
rq.requestTimer = time.AfterFunc(time.Until(deadline), rq.requestTimedOut)
rq.requestTimer = time.AfterFunc(
time.Until(deadline),
rq.requestTimedOut,
)
}
// requestTimedOut is called when SWRP_MAX_REQUEST_TIME_MS runs out while the
@@ -613,7 +648,9 @@ func (rq *request) setResponseDeadline() {
deadline := rq.endRequest().Add(limit)
_ = rq.rc.SetWriteDeadline(time.Now().Add(max(time.Until(deadline), minSendTime)))
_ = rq.rc.SetWriteDeadline(
time.Now().Add(max(time.Until(deadline), minSendTime)),
)
_ = rq.rc.Flush()
_ = rq.rc.SetWriteDeadline(deadline)
}
+4 -1
View File
@@ -78,7 +78,10 @@ type Server struct {
// on.
func New(params Params) *Server {
backend := params.Config.WAFBackend
errorLog := slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn)
errorLog := slog.NewLogLogger(
params.ProcessLog.Handler(),
slog.LevelWarn,
)
h := &handler{
config: params.Config,
requestLog: params.RequestLog,
+219 -76
View File
@@ -358,11 +358,13 @@ func startServerLogging(
}
maps.Copy(settings, env)
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
cfg, err := config.RProxyFromEnvironment(
func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
return value, ok
},
)
if err != nil {
t.Fatalf("settings %v: %v", settings, err)
}
@@ -370,10 +372,18 @@ func startServerLogging(
server := rproxy.New(rproxy.Params{
Config: cfg,
RequestLog: requestLog,
ProcessLog: requestlog.NewProcessLogger(processLog, "test", slog.LevelInfo),
ProcessLog: requestlog.NewProcessLogger(
processLog,
"test",
slog.LevelInfo,
),
})
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
listener, err := (&net.ListenConfig{}).Listen(
t.Context(),
"tcp",
localhost+":0",
)
if err != nil {
t.Fatalf("listen: %v", err)
}
@@ -403,7 +413,10 @@ type answered struct {
func visit(t *testing.T, req *http.Request) answered {
t.Helper()
transport := &http.Transport{DisableKeepAlives: true, DisableCompression: true}
transport := &http.Transport{
DisableKeepAlives: true,
DisableCompression: true,
}
defer transport.CloseIdleConnections()
res, err := transport.RoundTrip(req)
@@ -429,8 +442,12 @@ func newRequest(
) *http.Request {
t.Helper()
req, err := http.NewRequestWithContext(t.Context(), method, "http://"+addr+target,
body)
req, err := http.NewRequestWithContext(
t.Context(),
method,
"http://"+addr+target,
body,
)
if err != nil {
t.Fatalf("new request: %v", err)
}
@@ -450,7 +467,11 @@ func wantGreeting(t *testing.T, got answered) {
t.Helper()
if got.status != http.StatusOK || got.body != greeting {
t.Errorf("answered %d %q, want the app's greeting", got.status, got.body)
t.Errorf(
"answered %d %q, want the app's greeting",
got.status,
got.body,
)
}
}
@@ -547,7 +568,12 @@ func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
requestIDHeader: asked.header.Get(requestIDHeader),
} {
if received.Header.Get(name) != want {
t.Errorf("the app saw %s %q, want %q", name, received.Header.Get(name), want)
t.Errorf(
"the app saw %s %q, want %q",
name,
received.Header.Get(name),
want,
)
}
}
}
@@ -660,7 +686,12 @@ func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
})
_, _, server := startServer(t, standIn.URL, app.URL, nil)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
"/",
http.NoBody,
)
w := httptest.NewRecorder()
server.Handler.ServeHTTP(w, req)
@@ -670,8 +701,11 @@ func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
// stops it: the key with no value.
values, there := w.Header()["Content-Type"]
if !there || len(values) != 0 {
t.Errorf("the answer's Content-Type is %q, there: %v, want the key with no value",
values, there)
t.Errorf(
"the answer's Content-Type is %q, there: %v, want the key with no value",
values,
there,
)
}
}
@@ -706,8 +740,12 @@ func TestRemovesTheVisitorsLookupHeadersWhenWAFGivesNone(t *testing.T) {
wantGreeting(t, visit(t, req))
header := <-seen
if len(header.Values(asnHeader)) != 0 || len(header.Values(countryHeader)) != 0 {
t.Errorf("the app saw the visitor's lookup headers: %v", header)
if len(header.Values(asnHeader)) != 0 ||
len(header.Values(countryHeader)) != 0 {
t.Errorf(
"the app saw the visitor's lookup headers: %v",
header,
)
}
})
}
@@ -740,7 +778,8 @@ func TestDenyGivesTheVisitorWAFsAnswer(t *testing.T) {
}
line := out.requestLine(t)
if line["disposition"] != dispositionDeny || line["default_disposition"] != nil {
if line["disposition"] != dispositionDeny ||
line["default_disposition"] != nil {
t.Errorf("line %v, want waf's deny", line)
}
}
@@ -754,7 +793,11 @@ func TestADeniedHeadKeepsWAFsContentLength(t *testing.T) {
got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody))
if got.status != http.StatusForbidden ||
got.header.Get(contentLengthHeader) != strconv.Itoa(len(deniedBody)) {
got.header.Get(
contentLengthHeader,
) != strconv.Itoa(
len(deniedBody),
) {
t.Errorf("answered %+v, want waf's 403 and its length", got)
}
}
@@ -778,7 +821,14 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
log := heldLog{release: make(chan struct{})}
standIn := startWAF(t, deny)
app := startApp(t, greet)
addr, _ := startServerLogging(t, standIn.URL, app.URL, nil, log, io.Discard)
addr, _ := startServerLogging(
t,
standIn.URL,
app.URL,
nil,
log,
io.Discard,
)
t.Cleanup(func() { close(log.release) })
@@ -795,9 +845,16 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
}()
body, err := io.ReadAll(res.Body)
if err != nil || res.StatusCode != tc.status || string(body) != tc.body {
t.Errorf("answered %d %q (%v), want %d %q", res.StatusCode, body, err,
tc.status, tc.body)
if err != nil || res.StatusCode != tc.status ||
string(body) != tc.body {
t.Errorf(
"answered %d %q (%v), want %d %q",
res.StatusCode,
body,
err,
tc.status,
tc.body,
)
}
})
}
@@ -843,8 +900,11 @@ func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
text, err := io.ReadAll(conn)
if err != nil || len(text) != 0 {
t.Errorf("read %q (%v), want the connection closed without an answer",
text, err)
t.Errorf(
"read %q (%v), want the connection closed without an answer",
text,
err,
)
}
report := standIn.report(t)
@@ -858,7 +918,9 @@ func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
}
}
func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(
t *testing.T,
) {
t.Parallel()
backend := startWAFAnsweringReports(t,
@@ -881,7 +943,10 @@ func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
duration, ok := line["duration_response_ms"].(float64)
if !ok || duration < 0 {
t.Errorf("line %v, want a duration_response_ms that is not negative", line)
t.Errorf(
"line %v, want a duration_response_ms that is not negative",
line,
)
}
}
}
@@ -900,12 +965,16 @@ func answeringWith(answer http.HandlerFunc) func(t *testing.T) string {
func noAnswers() map[string]func(t *testing.T) string {
return map[string]func(t *testing.T) string{
"no connection": func(*testing.T) string { return nowhere },
"no answer at all": answeringWith(func(http.ResponseWriter, *http.Request) {
panic(http.ErrAbortHandler)
}),
"a slow start": answeringWith(func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
}),
"no answer at all": answeringWith(
func(http.ResponseWriter, *http.Request) {
panic(http.ErrAbortHandler)
},
),
"a slow start": answeringWith(
func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
},
),
"headers followed by a stalled body": answeringWith(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(dispositionHeader, dispositionDeny)
@@ -947,21 +1016,29 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsWAFGaveIt(t *testing.T) {
t.Run(tc.name+" with the default "+setting, func(t *testing.T) {
t.Parallel()
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
if tc.disposition != "" {
w.Header().Set(dispositionHeader, tc.disposition)
}
standIn := startWAF(
t,
func(w http.ResponseWriter, _ *http.Request) {
if tc.disposition != "" {
w.Header().Set(dispositionHeader, tc.disposition)
}
w.Header().Set(fromWAFHeader, addedHeaderValue)
w.WriteHeader(tc.status)
_, _ = io.WriteString(w, tc.body)
})
w.Header().Set(fromWAFHeader, addedHeaderValue)
w.WriteHeader(tc.status)
_, _ = io.WriteString(w, tc.body)
},
)
app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked")
})
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
defaultDisposition: setting,
})
addr, out := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
defaultDisposition: setting,
},
)
got := get(t, addr, "/")
if got.status != tc.status || got.body != tc.body ||
@@ -974,8 +1051,13 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsWAFGaveIt(t *testing.T) {
}
line := out.requestLine(t)
if line["status"] != float64(tc.status) || line["default_disposition"] != nil {
t.Errorf("line %v, want status %d and no default", line, tc.status)
if line["status"] != float64(tc.status) ||
line["default_disposition"] != nil {
t.Errorf(
"line %v, want status %d and no default",
line,
tc.status,
)
}
if out.has("waf gave no answer") {
@@ -1036,7 +1118,8 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
})
got := get(t, addr, "/")
if got.status != http.StatusInternalServerError || len(got.body) != size {
if got.status != http.StatusInternalServerError ||
len(got.body) != size {
t.Errorf("answered %d with %d bytes, want waf's 500 with %d",
got.status, len(got.body), size)
}
@@ -1048,7 +1131,11 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
func startWAFWriting(t *testing.T, text string) string {
t.Helper()
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
listener, err := (&net.ListenConfig{}).Listen(
t.Context(),
"tcp",
localhost+":0",
)
if err != nil {
t.Fatalf("listen: %v", err)
}
@@ -1126,10 +1213,15 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked")
})
addr, out := startRProxy(t, backend(t), app.URL, map[string]string{
timeoutSetting: "60000",
defaultDisposition: setting,
})
addr, out := startRProxy(
t,
backend(t),
app.URL,
map[string]string{
timeoutSetting: "60000",
defaultDisposition: setting,
},
)
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
t.Errorf("answered %+v, want 502", got)
@@ -1138,7 +1230,10 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
line := out.requestLine(t)
if line["status"] != float64(http.StatusBadGateway) ||
line["disposition"] != nil || line["default_disposition"] != nil {
t.Errorf("line %v, want status 502 and no disposition", line)
t.Errorf(
"line %v, want status 502 and no disposition",
line,
)
}
out.waitFor(t, "waf's answer is dropped")
@@ -1171,9 +1266,14 @@ func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
t.Parallel()
app := startApp(t, greet)
addr, out := startRProxy(t, backend(t), app.URL, map[string]string{
timeoutSetting: "200",
})
addr, out := startRProxy(
t,
backend(t),
app.URL,
map[string]string{
timeoutSetting: "200",
},
)
wantGreeting(t, get(t, addr, "/"))
@@ -1190,10 +1290,15 @@ func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked")
})
addr, out := startRProxy(t, backend(t), app.URL, map[string]string{
timeoutSetting: "200",
defaultDisposition: dispositionDeny,
})
addr, out := startRProxy(
t,
backend(t),
app.URL,
map[string]string{
timeoutSetting: "200",
defaultDisposition: dispositionDeny,
},
)
got := get(t, addr, "/")
if got.status != http.StatusServiceUnavailable {
@@ -1216,16 +1321,24 @@ func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDefault(t *testing.T) {
t.Run("with the default "+disposition, func(t *testing.T) {
t.Parallel()
standIn := startWAF(t, func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
})
standIn := startWAF(
t,
func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
},
)
app := startApp(t, func(http.ResponseWriter, *http.Request) {
t.Error("the app was asked")
})
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
timeoutSetting: "60000",
defaultDisposition: disposition,
})
addr, out := startRProxy(
t,
standIn.URL,
app.URL,
map[string]string{
timeoutSetting: "60000",
defaultDisposition: disposition,
},
)
conn := dial(t, addr)
send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
@@ -1443,12 +1556,21 @@ func TestReportGivesWhatHappened(t *testing.T) {
report.ResponseBytes != int64(len(greeting)) ||
report.DurationTotalMS <= 0 || report.DurationUpstreamTotalMS <= 0 ||
report.DurationUpstreamTotalMS > report.DurationTotalMS {
t.Errorf("report %+v, of %d bytes up and %d back", report, len(body), len(greeting))
t.Errorf(
"report %+v, of %d bytes up and %d back",
report,
len(body),
len(greeting),
)
}
line := out.requestLine(t)
if line["request_id"] != report.RequestID {
t.Errorf("line %v, report %+v, want the same request id", line, report)
t.Errorf(
"line %v, report %+v, want the same request id",
line,
report,
)
}
}
@@ -1477,9 +1599,14 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
t.Cleanup(backend.Close)
app := startApp(t, greet)
addr, _, server := startServer(t, backend.URL, app.URL, map[string]string{
timeoutSetting: "5000",
})
addr, _, server := startServer(
t,
backend.URL,
app.URL,
map[string]string{
timeoutSetting: "5000",
},
)
wantGreeting(t, get(t, addr, "/"))
@@ -1535,7 +1662,11 @@ func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
_ = res.Body.Close()
if res.StatusCode != http.StatusSwitchingProtocols {
t.Fatalf("status %d, want %d", res.StatusCode, http.StatusSwitchingProtocols)
t.Fatalf(
"status %d, want %d",
res.StatusCode,
http.StatusSwitchingProtocols,
)
}
// Past both time limits, the connection still carries what is sent.
@@ -1559,7 +1690,9 @@ func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
t.Errorf("report %+v, of %d bytes each way", report, len(message))
}
if line := out.requestLine(t); line["status"] != float64(res.StatusCode) {
if line := out.requestLine(t); line["status"] != float64(
res.StatusCode,
) {
t.Errorf("line %v, want status 101", line)
}
}
@@ -1714,7 +1847,8 @@ func TestEachRequestGetsAFreshID(t *testing.T) {
ids = append(ids, standIn.question(t).header.Get(requestIDHeader))
}
if slices.Contains(ids, fromTraefik) || ids[0] == "" || ids[0] == ids[1] {
if slices.Contains(ids, fromTraefik) || ids[0] == "" ||
ids[0] == ids[1] {
t.Errorf("ids %v, want two fresh ones", ids)
}
}
@@ -1732,7 +1866,13 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
app := startApp(t, greet)
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
req := newRequest(t, http.MethodPost, addr, path+"?"+query, strings.NewReader(body))
req := newRequest(
t,
http.MethodPost,
addr,
path+"?"+query,
strings.NewReader(body),
)
req.Host = site
wantGreeting(t, visit(t, req))
@@ -1758,7 +1898,10 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
}
if line["default_disposition"] != nil {
t.Errorf("default_disposition is %v, want it left out", line["default_disposition"])
t.Errorf(
"default_disposition is %v, want it left out",
line["default_disposition"],
)
}
for _, name := range []string{"duration_request_ms", "duration_response_ms"} {
+24 -7
View File
@@ -23,7 +23,9 @@ const answerBodyMaxBytes = 1 << 20
const reportAnswerMaxBytes = 4 << 10
var (
errCannotPassOn = errors.New("the answer cannot be passed to the visitor")
errCannotPassOn = errors.New(
"the answer cannot be passed to the visitor",
)
errReportRefused = errors.New("the report was answered")
)
@@ -108,7 +110,11 @@ func (rq *request) ask(ctx context.Context) (*wafAnswer, error) {
// a read of that. Go's client also takes a status below 100, which Go's server
// panics when asked to send.
if res.StatusCode < http.StatusOK {
return nil, fmt.Errorf("%w: its status is %d", errCannotPassOn, res.StatusCode)
return nil, fmt.Errorf(
"%w: its status is %d",
errCannotPassOn,
res.StatusCode,
)
}
body, err := io.ReadAll(io.LimitReader(res.Body, answerBodyMaxBytes+1))
@@ -133,10 +139,14 @@ func (rq *request) ask(ctx context.Context) (*wafAnswer, error) {
// waf.
func canBeAsked(r *http.Request) bool {
target, host := r.RequestURI, r.Host
hostHasZone := strings.HasPrefix(host, "[") && strings.Contains(host, "%")
hostHasZone := strings.HasPrefix(host, "[") &&
strings.Contains(host, "%")
return strings.HasPrefix(target, "/") && !strings.HasPrefix(target, "//") &&
host != "" && !hostHasZone && len(r.Header.Values("User-Agent")) <= 1
return strings.HasPrefix(target, "/") &&
!strings.HasPrefix(target, "//") &&
host != "" &&
!hostHasZone &&
len(r.Header.Values("User-Agent")) <= 1
}
// newQuestion returns the question about the request, as "The question" in
@@ -151,7 +161,11 @@ func (rq *request) newQuestion(ctx context.Context) (*http.Request, error) {
}
question, err := http.NewRequestWithContext( //nolint:gosec // a setting's URL
ctx, in.Method, rq.h.config.WAFBackend.String(), bytes.NewReader(held))
ctx,
in.Method,
rq.h.config.WAFBackend.String(),
bytes.NewReader(held),
)
if err != nil {
return nil, fmt.Errorf("make the question: %w", err)
}
@@ -254,7 +268,10 @@ func (h *handler) postReport(ctx context.Context, report *waf.Report) error {
// The body of a 101 is the connection itself, and the time limit does not end
// a read of that.
if res.StatusCode >= http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(res.Body, reportAnswerMaxBytes))
_, _ = io.Copy(
io.Discard,
io.LimitReader(res.Body, reportAnswerMaxBytes),
)
}
_ = res.Body.Close()