diff --git a/internal/rproxy/limits_test.go b/internal/rproxy/limits_test.go index 685f05b..2af3b1f 100644 --- a/internal/rproxy/limits_test.go +++ b/internal/rproxy/limits_test.go @@ -12,6 +12,7 @@ import ( "os" "strconv" "strings" + "sync" "sync/atomic" "testing" "time" @@ -807,8 +808,10 @@ func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing. app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, greeting) }) + // The request time limit is far longer than dial's deadline, so that a busy + // host cannot use it up before the app's answer arrives. addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ - maxRequestTime: "3000", + maxRequestTime: "120000", }) // Far more announced than sent, so that Go's server answers for the app @@ -869,11 +872,11 @@ func TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce(t *testing.T) { standIn := startWAF(t, allow) app := startApp(t, answerBeforeTheBody) - // The request time limit is longer than the 10 seconds the status and headers - // of an answer have to be sent, and than dial's deadline. + // Both time limits are far longer than dial's deadline, so that the answer + // has to come before either runs out, and a busy host cannot use them up. addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ - maxRequestTime: "15000", - maxResponseTime: "300", + maxRequestTime: "120000", + maxResponseTime: "60000", }) conn := dial(t, addr) @@ -910,6 +913,82 @@ func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(t *testing. } } +func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOff( + t *testing.T, +) { + t.Parallel() + + const limit = 300 * time.Millisecond + + // With two parts, rproxy fails to send the first and then waits for the + // second, which the app sends only once rproxy has ended the request to it. + for _, tc := range []struct { + name string + parts []string + }{ + {"with the body in one part", []string{greeting}}, + {"with the body in two parts", []string{greeting[:5], greeting[5:]}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + headersRead := make(chan struct{}) + // letAppSendBody also runs when the test ends, as the app's server waits + // for the app. + letAppSendBody := sync.OnceFunc(func() { close(headersRead) }) + standIn := startWAF(t, allow) + // The app sends its body once the visitor has the status and headers and + // the response time limit has run out since, so that rproxy cannot send + // it. After each part, it reads the request's body, which ends when rproxy + // ends the request to it. + app := startApp(t, func(w http.ResponseWriter, r *http.Request) { + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() + + w.Header().Set(contentLengthHeader, strconv.Itoa(len(greeting))) + + _ = rc.Flush() + + <-headersRead + time.Sleep(limit) + + for _, part := range tc.parts { + _, _ = io.WriteString(w, part) + _ = rc.Flush() + _, _ = io.Copy(io.Discard, r.Body) + } + }) + + t.Cleanup(letAppSendBody) + + // 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), + }) + + conn := dial(t, addr) + sendAllButALittle(t, conn) + + res, err := http.ReadResponse(bufio.NewReader(conn), nil) + if err != nil { + t.Fatalf("read the answer: %v", err) + } + + letAppSendBody() + + // If the connection stays open, dial's deadline ends this read with a + // timeout. + body, err := io.ReadAll(res.Body) + _ = res.Body.Close() + + if len(body) != 0 || !errors.Is(err, io.ErrUnexpectedEOF) { + t.Errorf("read %q (%v), want the connection closed", body, err) + } + }) + } +} + func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) { t.Parallel()