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 is contained in:
1 parent
f9bec5146b
commit
a8b24a843b
117 files changed
+9441
-2670
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user