Skip to content

Commit

Permalink
feat(testingx): add HTTPHandlerResetWhileReadingBody (#1563)
Browse files Browse the repository at this point in the history
Closes ooni/probe#2714

Extracted from #1560

Part of ooni/probe#2700
  • Loading branch information
bassosimone authored Apr 23, 2024
1 parent 6bd35a2 commit 51b185b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 8 deletions.
48 changes: 40 additions & 8 deletions internal/testingx/httptestx.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"net"
"net/http"
"net/url"
"time"

"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/randx"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)

Expand Down Expand Up @@ -173,14 +175,7 @@ func HTTPHandlerTimeout() http.Handler {
}

func httpHandlerHijack(w http.ResponseWriter, r *http.Request, policy string) {
// Note:
//
// 1. we assume we can hihack the connection
//
// 2. Hijack won't fail the first time it's invoked
hijacker := w.(http.Hijacker)
conn, _ := runtimex.Try2(hijacker.Hijack())

conn := httpHijack(w)
defer conn.Close()

switch policy {
Expand All @@ -194,3 +189,40 @@ func httpHandlerHijack(w http.ResponseWriter, r *http.Request, policy string) {
// nothing
}
}

// HTTPHandlerResetWhileReadingBody returns a handler that sends a
// connection reset by peer while the client is reading the body.
func HTTPHandlerResetWhileReadingBody() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn := httpHijack(w)
defer conn.Close()

// write the HTTP response headers
conn.Write([]byte("HTTP/1.1 200 Ok\r\n"))
conn.Write([]byte("Content-Type: text/html\r\n"))
conn.Write([]byte("Content-Length: 65535\r\n"))
conn.Write([]byte("\r\n"))

// start writing the response
content := randx.Letters(32768)
conn.Write([]byte(content))

// sleep for half a second simulating something wrong
time.Sleep(500 * time.Millisecond)

// finally issue reset for the conn
tcpMaybeResetNetConn(conn)
})
}

// httpHijack is a convenience function to hijack the underlying connection.
func httpHijack(w http.ResponseWriter) net.Conn {
// Note:
//
// 1. we assume we can hihack the connection
//
// 2. Hijack won't fail the first time it's invoked
hijacker := w.(http.Hijacker)
conn, _ := runtimex.Try2(hijacker.Hijack())
return conn
}
40 changes: 40 additions & 0 deletions internal/testingx/httptestx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,43 @@ func TestHTTPTestxWithNetem(t *testing.T) {
})
}
}

func TestHTTPHandlerResetWhileReadingBody(t *testing.T) {
// create a server for testing the given handler
server := testingx.MustNewHTTPServer(testingx.HTTPHandlerResetWhileReadingBody())
defer server.Close()

// create a suitable HTTP transport using netxlite
netx := &netxlite.Netx{Underlying: nil}
dialer := netx.NewDialerWithoutResolver(log.Log)
handshaker := netx.NewTLSHandshakerStdlib(log.Log)
tlsDialer := netxlite.NewTLSDialer(dialer, handshaker)
txp := netxlite.NewHTTPTransportWithOptions(log.Log, dialer, tlsDialer)

// create the request
req := runtimex.Try1(http.NewRequest("GET", server.URL, nil))

// perform the round trip
resp, err := txp.RoundTrip(req)

// we do not expect an error during the round trip
if err != nil {
t.Fatal(err)
}

// make sure we close the body
defer resp.Body.Close()

// start reading the response where we expect to see a RST
respbody, err := netxlite.ReadAllContext(req.Context(), resp.Body)

// verify we received a connection reset
if !errors.Is(err, netxlite.ECONNRESET) {
t.Fatal("expected ECONNRESET, got", err)
}

// make sure we've got no bytes
if len(respbody) != 0 {
t.Fatal("expected to see zero bytes here")
}
}

0 comments on commit 51b185b

Please sign in to comment.