Skip to content

Commit

Permalink
http2/h2c: handle request bodies during h2c connection upgrading
Browse files Browse the repository at this point in the history
If a request that triggered an upgrade from HTTP/1.1 -> HTTP/2
contained a body, it would not be replayed by the server as a HTTP/2 data frame.
This would result in hangs as the client would get no data back,
as the request body was never actually handled.

This code corrects this, and sends HTTP/2 DATA frames with the request body.

As an example:

Client:
```
$ curl -v --http2 -d 'POST BODY' http://localhost:5555
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5555 (#0)
> POST / HTTP/1.1
> Host: localhost:5555
> User-Agent: curl/7.64.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 9 out of 9 bytes
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-length: 0
< date: Sat, 29 Jan 2022 06:51:05 GMT
<
* Connection #0 to host localhost left intact
* Closing connection 0
```

Echo server:
```
$ ./bin/h2test
Listening [0.0.0.0:5555]...
Request: {Method:POST URL:/ Proto:HTTP/2.0 ProtoMajor:2 ProtoMinor:0 Header:map[Accept:[*/*]
Content-Length:[9] Content-Type:[application/x-www-form-urlencoded] User-Agent:[curl/7.64.1]]
Body:0xc000098120 GetBody:<nil> ContentLength:9 TransferEncoding:[] Close:false Host:localhost:5555
Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr:127.0.0.1:54540 RequestURI:/
TLS:<nil> Cancel:<nil> Response:<nil> ctx:0xc0000a0000}

Received body: POST BODY
```

Fixes #38064
  • Loading branch information
jlamanna committed Feb 4, 2022
1 parent cd36cc0 commit 189b99a
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 0 deletions.
32 changes: 32 additions & 0 deletions http2/h2c/h2c.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,38 @@ func convertH1ReqToH2(r *http.Request) (*bytes.Buffer, []http2.Setting, error) {
}
}

// Any request body create as DATA frames
if r.Body != nil && r.Body != http.NoBody {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, nil, fmt.Errorf("Could not read request body: %v", err)
}

needOneDataFrame := len(body) < maxFrameSize
err = framer.WriteData(1,
needOneDataFrame, // end stream?
body)
if err != nil {
return nil, nil, err
}

for i := maxFrameSize; i < len(body); i += maxFrameSize {
if len(body)-i > maxFrameSize {
if err := framer.WriteData(1,
false, // end stream?
body[i:maxFrameSize]); err != nil {
return nil, nil, err
}
} else {
if err := framer.WriteData(1,
true, // end stream?
body[i:]); err != nil {
return nil, nil, err
}
}
}
}

return h2Bytes, settings, nil
}

Expand Down
76 changes: 76 additions & 0 deletions http2/h2c/h2c_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,79 @@ func TestContext(t *testing.T) {
t.Fatal(err)
}
}

func Test_convertH1ReqToH2_with_POST(t *testing.T) {
postBody := "Some POST Body"

r, err := http.NewRequest("POST", "http://localhost:80", bytes.NewBufferString(postBody))
if err != nil {
t.Fatal(err)
}

r.Header.Set("Upgrade", "h2c")
r.Header.Set("Connection", "Upgrade, HTTP2-Settings")
r.Header.Set("HTTP2-Settings", "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA") // Some Default Settings
h2Bytes, _, err := convertH1ReqToH2(r)

if err != nil {
t.Fatal(err)
}

// Read off the preface
preface := []byte(http2.ClientPreface)
if h2Bytes.Len() < len(preface) {
t.Fatal("Could not read HTTP/2 ClientPreface")
}
readPreface := h2Bytes.Next(len(preface))
if string(readPreface) != http2.ClientPreface {
t.Fatalf("Expected Preface %s but got: %s", http2.ClientPreface, string(readPreface))
}

framer := http2.NewFramer(nil, h2Bytes)

// Should get a SETTINGS, HEADERS, and then DATA
expectedFrameTypes := []http2.FrameType{http2.FrameSettings, http2.FrameHeaders, http2.FrameData}
for frameNumber := 0; h2Bytes.Len() > 0; {
frame, err := framer.ReadFrame()
if err != nil {
t.Fatal(err)
}

if frameNumber >= len(expectedFrameTypes) {
t.Errorf("Got more than %d frames, wanted only %d", len(expectedFrameTypes), len(expectedFrameTypes))
}

if frame.Header().Type != expectedFrameTypes[frameNumber] {
t.Errorf("Got FrameType %v, wanted %v", frame.Header().Type, expectedFrameTypes[frameNumber])
}

frameNumber += 1

switch f := frame.(type) {
case *http2.SettingsFrame:
if frameNumber != 1 {
t.Errorf("Got SETTINGS frame as frame #%d, wanted it as frame #1", frameNumber)
}
case *http2.HeadersFrame:
if frameNumber != 2 {
t.Errorf("Got HEADERS frame as frame #%d, wanted it as frame #2", frameNumber)
}
if f.FrameHeader.StreamID != 1 {
t.Fatalf("Expected StreamId 1, got %v", f.FrameHeader.StreamID)
}
case *http2.DataFrame:
if frameNumber != 3 {
t.Errorf("Got DATA frame as frame #%d, wanted it as frame #3", frameNumber)
}
if f.FrameHeader.StreamID != 1 {
t.Errorf("Got StreamID %v, wanted 1", f.FrameHeader.StreamID)
}

body := string(f.Data())

if body != postBody {
t.Errorf("Got DATA body %s, wanted %s", body, postBody)
}
}
}
}

0 comments on commit 189b99a

Please sign in to comment.