rproxy: read the rest of a Connection: close visitor's body after an early answer (closes #373)
check / check (push) Waiting to run
check / check (push) Waiting to run
Go's server reads up to 256 KiB of a body left unread once the handler returns, but not a body with a declared length from a visitor that sent Connection: close. It closes that connection with the rest unread, and the visitor, still sending, gets a reset. rproxy now reads that rest itself, until the deadline already set on the connection, through a MaxBytesReader made with Go's own ResponseWriter. With more than 256 KiB left, that has Go's server tell the visitor it sends no more and wait 500 ms before it closes, as for any other visitor. Model: opus-5-5
This commit is contained in:
1 parent
c4bf76c966
commit
4853a5c592
3 files changed
+99
-2
No files matched your search
@@ -136,6 +136,73 @@ func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAVisitorSendingConnectionCloseReadsAnAnswerBeforeTheRestOfItsBody(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
waf http.HandlerFunc
|
||||
rest int
|
||||
status int
|
||||
}{
|
||||
{"over the size limit", allow, 65 << 10, http.StatusRequestEntityTooLarge},
|
||||
{"denied", deny, 65 << 10, http.StatusForbidden},
|
||||
// rproxy reads 256 KiB of this rest. What it leaves unread must fit in the
|
||||
// connection's buffers. Otherwise the visitor is still sending when the
|
||||
// connection closes, and gets a reset.
|
||||
{
|
||||
"with more than 256 KiB left", allow, 272 << 10,
|
||||
http.StatusRequestEntityTooLarge,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
standIn := startWAF(t, tc.waf)
|
||||
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: "128K"})
|
||||
|
||||
conn := dial(t, addr)
|
||||
send(t, conn, post+"Connection: close\r\n"+
|
||||
announcedLength(questionBodyMaxBytes+tc.rest)+
|
||||
strings.Repeat("x", questionBodyMaxBytes))
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
res, err := http.ReadResponse(reader, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("read the answer: %v", err)
|
||||
}
|
||||
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
|
||||
if res.StatusCode != tc.status {
|
||||
t.Errorf("status %d, want %d", res.StatusCode, tc.status)
|
||||
}
|
||||
|
||||
// The rest goes in small parts, so that a reset fails a later one.
|
||||
for sent := 0; sent < tc.rest; sent += 1 << 10 {
|
||||
send(t, conn, strings.Repeat("x", 1<<10))
|
||||
}
|
||||
|
||||
got, err := io.ReadAll(reader)
|
||||
if err != nil || len(got) != 0 {
|
||||
t.Errorf(
|
||||
"after the answer, read %q and %v, want the end",
|
||||
got,
|
||||
err,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in new issue
Block a user