[net] http2/h2c: handle request bodies during h2c connection upgrading

137 views
Skip to first unread message

Gerrit Bot (Gerrit)

unread,
Feb 4, 2022, 1:24:50 AM2/4/22
to goph...@pubsubhelper.golang.org, James Lamanna, golang-co...@googlegroups.com

Gerrit Bot has uploaded this change for review.

View Change

http2/h2c: handle request bodies during h2c connection upgrading

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

Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
GitHub-Last-Rev: 189b99aa2b1cd8ee48efdeba6592902c5e28604e
GitHub-Pull-Request: golang/net#128
---
M http2/h2c/h2c.go
M http2/h2c/h2c_test.go
2 files changed, 176 insertions(+), 0 deletions(-)

diff --git a/http2/h2c/h2c.go b/http2/h2c/h2c.go
index c0970d8..3dba917 100644
--- a/http2/h2c/h2c.go
+++ b/http2/h2c/h2c.go
@@ -249,6 +249,38 @@
}
}

+ // 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
}

diff --git a/http2/h2c/h2c_test.go b/http2/h2c/h2c_test.go
index bd9461e..201046e 100644
--- a/http2/h2c/h2c_test.go
+++ b/http2/h2c/h2c_test.go
@@ -104,3 +104,79 @@
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)
+ }
+ }
+ }
+}

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 1
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-MessageType: newchange

Ingo Oeser (Gerrit)

unread,
Feb 4, 2022, 2:45:05 AM2/4/22
to Gerrit Bot, James Lamanna, goph...@pubsubhelper.golang.org, Damien Neil, Tom Bergan, Gopher Robot, golang-co...@googlegroups.com

Attention is currently required from: Damien Neil, Tom Bergan.

View Change

2 comments:

  • Patchset:

    • Patch Set #1:

      Noticed an issue when streaming large request bodies.

  • File http2/h2c/h2c.go:

    • Patch Set #1, Line 254: body, err := io.ReadAll(r.Body)

      io.ReadAll is dangerous here as that might read gigabytes of data into memory leading to a crash due to out of memory. Please use a read until maxFrameSize only.

      I wonder whether this all could look more like the core of io.CopyBuffer or even use that.

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 1
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Damien Neil <dn...@google.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-Comment-Date: Fri, 04 Feb 2022 07:43:27 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Gerrit-MessageType: comment

Gerrit Bot (Gerrit)

unread,
Feb 4, 2022, 9:57:07 PM2/4/22
to James Lamanna, goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Attention is currently required from: Damien Neil, Tom Bergan.

Gerrit Bot uploaded patch set #2 to this change.

View Change

GitHub-Last-Rev: ba79222830de46c3accddb442b867c4ca37a7635

GitHub-Pull-Request: golang/net#128
---
M http2/h2c/h2c.go
M http2/h2c/h2c_test.go
2 files changed, 268 insertions(+), 0 deletions(-)

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 2
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Damien Neil <dn...@google.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-MessageType: newpatchset

Damien Neil (Gerrit)

unread,
Feb 17, 2022, 2:14:56 PM2/17/22
to Gerrit Bot, James Lamanna, goph...@pubsubhelper.golang.org, Ingo Oeser, Tom Bergan, Gopher Robot, golang-co...@googlegroups.com

Attention is currently required from: Tom Bergan.

Patch set 2:Code-Review -1

View Change

1 comment:

  • File http2/h2c/h2c.go:

    • Patch Set #2, Line 255: for {

      This no longer uses io.ReadAll, but it still reads the entire request body into memory; it's still as dangerous as before.

      I don't think this is the right approach. We can't read the entire request. Converting an HTTP/1 request body into an HTTP/2 one seems very tricky; while I haven't dug into this code to verify it, I strongly suspect that flow control will get very confused: The HTTP/2 server will think that the initial HTTP/1 request is consuming flow control tokens.

      I wonder if the correct approach is to stop trying to translate the initial HTTP/1.1 request into HTTP/2.

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 2
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-Comment-Date: Thu, 17 Feb 2022 19:14:51 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
Gerrit-MessageType: comment

Damien Neil (Gerrit)

unread,
Feb 22, 2022, 8:44:38 PM2/22/22
to Gerrit Bot, James Lamanna, goph...@pubsubhelper.golang.org, Ingo Oeser, Tom Bergan, Gopher Robot, golang-co...@googlegroups.com

Attention is currently required from: Ingo Oeser, Tom Bergan, James Lamanna.

View Change

1 comment:

  • File http2/h2c/h2c.go:

    • This only reads maxFrameSize at a time, it does not try and read the entire body if its > maxFrameSi […]

      Unless I'm misunderstanding something, this loops for as long as n==maxFrameSize, calling framer.WriteData for each frame. WriteData buffers the data.

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 2
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Ingo Oeser <night...@googlemail.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-Attention: James Lamanna <jlam...@gmail.com>
Gerrit-Comment-Date: Wed, 23 Feb 2022 01:44:34 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Damien Neil <dn...@google.com>
Comment-In-Reply-To: James Lamanna <jlam...@gmail.com>
Gerrit-MessageType: comment

James Lamanna (Gerrit)

unread,
Feb 23, 2022, 12:04:40 PM2/23/22
to Gerrit Bot, goph...@pubsubhelper.golang.org, Damien Neil, Ingo Oeser, Tom Bergan, Gopher Robot, golang-co...@googlegroups.com

Attention is currently required from: Ingo Oeser, Damien Neil, Tom Bergan.

View Change

2 comments:

  • File http2/h2c/h2c.go:

    • io. […]

      Updated in rev2.

  • File http2/h2c/h2c.go:

    • This no longer uses io. […]

      This only reads maxFrameSize at a time, it does not try and read the entire body if its > maxFrameSize.

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 2
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Ingo Oeser <night...@googlemail.com>
Gerrit-Attention: Damien Neil <dn...@google.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-Comment-Date: Wed, 23 Feb 2022 01:02:22 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Ingo Oeser <night...@googlemail.com>
Comment-In-Reply-To: Damien Neil <dn...@google.com>
Gerrit-MessageType: comment

James Lamanna (Gerrit)

unread,
Apr 13, 2022, 10:07:33 AM4/13/22
to Gerrit Bot, goph...@pubsubhelper.golang.org, Damien Neil, Ingo Oeser, Tom Bergan, Gopher Robot, golang-co...@googlegroups.com

Attention is currently required from: Ingo Oeser, Damien Neil, Tom Bergan.

View Change

1 comment:

  • File http2/h2c/h2c.go:

    • Unless I'm misunderstanding something, this loops for as long as n==maxFrameSize, calling framer. […]

      WriteData() calls ioWriter.Write(). If you consider that buffering, than sure, but I'm not sure how else you would ever write out large amounts of data either here or in a different DATA frame path.

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 2
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Ingo Oeser <night...@googlemail.com>
Gerrit-Attention: Damien Neil <dn...@google.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-Comment-Date: Wed, 13 Apr 2022 14:07:28 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No

Damien Neil (Gerrit)

unread,
Apr 15, 2022, 11:35:00 AM4/15/22
to Gerrit Bot, James Lamanna, goph...@pubsubhelper.golang.org, Ingo Oeser, Tom Bergan, Gopher Robot, golang-co...@googlegroups.com

Attention is currently required from: Ingo Oeser, Tom Bergan, James Lamanna.

View Change

1 comment:

  • File http2/h2c/h2c.go:

    • WriteData() calls ioWriter.Write(). […]

      framer.WriteData calls framer.w.Write. f.w is a bytes.Buffer created on line 209 of this file. Writing to the framer buffers the data into that bytes.Buffer.

      Or am I missing something?

To view, visit change 383114. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I52b6115a3c9551f7802f0c7c18d948dbf34f81be
Gerrit-Change-Number: 383114
Gerrit-PatchSet: 2
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Tom Bergan <tomb...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-CC: Ingo Oeser <night...@googlemail.com>
Gerrit-CC: James Lamanna <jlam...@gmail.com>
Gerrit-Attention: Ingo Oeser <night...@googlemail.com>
Gerrit-Attention: Tom Bergan <tomb...@google.com>
Gerrit-Attention: James Lamanna <jlam...@gmail.com>
Gerrit-Comment-Date: Fri, 15 Apr 2022 15:34:57 +0000
Reply all
Reply to author
Forward
0 new messages