Skip to content

Commit 43a20da

Browse files
committed
net/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
1 parent cd36cc0 commit 43a20da

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

http2/h2c/h2c.go

+32
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,38 @@ func convertH1ReqToH2(r *http.Request) (*bytes.Buffer, []http2.Setting, error) {
249249
}
250250
}
251251

252+
// Any request body create as DATA frames
253+
if r.Body != nil && r.Body != http.NoBody {
254+
body, err := io.ReadAll(r.Body)
255+
if err != nil {
256+
return nil, nil, fmt.Errorf("Could not read request body: %v", err)
257+
}
258+
259+
needOneDataFrame := len(body) < maxFrameSize
260+
err = framer.WriteData(1,
261+
needOneDataFrame, // end stream?
262+
body)
263+
if err != nil {
264+
return nil, nil, err
265+
}
266+
267+
for i := maxFrameSize; i < len(body); i += maxFrameSize {
268+
if len(body)-i > maxFrameSize {
269+
if err := framer.WriteData(1,
270+
false, // end stream?
271+
body[i:maxFrameSize]); err != nil {
272+
return nil, nil, err
273+
}
274+
} else {
275+
if err := framer.WriteData(1,
276+
true, // end stream?
277+
body[i:]); err != nil {
278+
return nil, nil, err
279+
}
280+
}
281+
}
282+
}
283+
252284
return h2Bytes, settings, nil
253285
}
254286

http2/h2c/h2c_test.go

+76
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,79 @@ func TestContext(t *testing.T) {
104104
t.Fatal(err)
105105
}
106106
}
107+
108+
func Test_convertH1ReqToH2_with_POST(t *testing.T) {
109+
postBody := "Some POST Body"
110+
111+
r, err := http.NewRequest("POST", "http://localhost:80", bytes.NewBufferString(postBody))
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
116+
r.Header.Set("Upgrade", "h2c")
117+
r.Header.Set("Connection", "Upgrade, HTTP2-Settings")
118+
r.Header.Set("HTTP2-Settings", "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA") // Some Default Settings
119+
h2Bytes, _, err := convertH1ReqToH2(r)
120+
121+
if err != nil {
122+
t.Fatal(err)
123+
}
124+
125+
// Read off the preface
126+
preface := []byte(http2.ClientPreface)
127+
if h2Bytes.Len() < len(preface) {
128+
t.Fatal("Could not read HTTP/2 ClientPreface")
129+
}
130+
readPreface := h2Bytes.Next(len(preface))
131+
if string(readPreface) != http2.ClientPreface {
132+
t.Fatalf("Expected Preface %s but got: %s", http2.ClientPreface, string(readPreface))
133+
}
134+
135+
framer := http2.NewFramer(nil, h2Bytes)
136+
137+
// Should get a SETTINGS, HEADERS, and then DATA
138+
expectedFrameTypes := []http2.FrameType{http2.FrameSettings, http2.FrameHeaders, http2.FrameData}
139+
for frameNumber := 0; h2Bytes.Len() > 0; {
140+
frame, err := framer.ReadFrame()
141+
if err != nil {
142+
t.Fatal(err)
143+
}
144+
145+
if frameNumber >= len(expectedFrameTypes) {
146+
t.Errorf("Got more than %d frames, wanted only %d", len(expectedFrameTypes), len(expectedFrameTypes))
147+
}
148+
149+
if frame.Header().Type != expectedFrameTypes[frameNumber] {
150+
t.Errorf("Got FrameType %v, wanted %v", frame.Header().Type, expectedFrameTypes[frameNumber])
151+
}
152+
153+
frameNumber += 1
154+
155+
switch f := frame.(type) {
156+
case *http2.SettingsFrame:
157+
if frameNumber != 1 {
158+
t.Errorf("Got SETTINGS frame as frame #%d, wanted it as frame #1", frameNumber)
159+
}
160+
case *http2.HeadersFrame:
161+
if frameNumber != 2 {
162+
t.Errorf("Got HEADERS frame as frame #%d, wanted it as frame #2", frameNumber)
163+
}
164+
if f.FrameHeader.StreamID != 1 {
165+
t.Fatalf("Expected StreamId 1, got %v", f.FrameHeader.StreamID)
166+
}
167+
case *http2.DataFrame:
168+
if frameNumber != 3 {
169+
t.Errorf("Got DATA frame as frame #%d, wanted it as frame #3", frameNumber)
170+
}
171+
if f.FrameHeader.StreamID != 1 {
172+
t.Errorf("Got StreamID %v, wanted 1", f.FrameHeader.StreamID)
173+
}
174+
175+
body := string(f.Data())
176+
177+
if body != postBody {
178+
t.Errorf("Got DATA body %s, wanted %s", body, postBody)
179+
}
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)