diff --git a/connection/quic.go b/connection/quic.go index c4f7e0ae..fa5136d9 100644 --- a/connection/quic.go +++ b/connection/quic.go @@ -180,6 +180,20 @@ func buildHTTPRequest(connectRequest *quicpogs.ConnectRequest, body io.Reader) ( req.Header.Add(httpHeaderKey[1], metadata.Val) } } + // Go's http.Client automatically sends chunked request body if this value is not set on the + // *http.Request struct regardless of header: + // https://go.googlesource.com/go/+/go1.8rc2/src/net/http/transfer.go#154. + if err := setContentLength(req); err != nil { + return nil, fmt.Errorf("Error setting content-length: %w", err) + } stripWebsocketUpgradeHeader(req) return req, err } + +func setContentLength(req *http.Request) error { + var err error + if contentLengthStr := req.Header.Get("Content-Length"); contentLengthStr != "" { + req.ContentLength, err = strconv.ParseInt(contentLengthStr, 10, 64) + } + return err +} diff --git a/connection/quic_test.go b/connection/quic_test.go index 332eb987..60040cb2 100644 --- a/connection/quic_test.go +++ b/connection/quic_test.go @@ -13,6 +13,7 @@ import ( "math/big" "net" "net/http" + "net/url" "os" "sync" "testing" @@ -292,6 +293,107 @@ func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Reque return nil } +func TestBuildHTTPRequest(t *testing.T) { + var tests = []struct { + name string + connectRequest *quicpogs.ConnectRequest + req *http.Request + }{ + { + name: "check if http.Request is built correctly with content length", + connectRequest: &quicpogs.ConnectRequest{ + Dest: "http://test.com", + Metadata: []quicpogs.Metadata{ + quicpogs.Metadata{ + Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade", + Val: "Websocket", + }, + quicpogs.Metadata{ + Key: "HttpHeader:Content-Length", + Val: "514", + }, + quicpogs.Metadata{ + Key: "HttpHeader:Another-Header", + Val: "Misc", + }, + quicpogs.Metadata{ + Key: "HttpHost", + Val: "cf.host", + }, + quicpogs.Metadata{ + Key: "HttpMethod", + Val: "get", + }, + }, + }, + req: &http.Request{ + Method: "get", + URL: &url.URL{ + Scheme: "http", + Host: "test.com", + }, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{ + "Another-Header": []string{"Misc"}, + "Content-Length": []string{"514"}, + }, + ContentLength: 514, + Host: "cf.host", + }, + }, + { + name: "if content length isn't part of request headers, then it's not set", + connectRequest: &quicpogs.ConnectRequest{ + Dest: "http://test.com", + Metadata: []quicpogs.Metadata{ + quicpogs.Metadata{ + Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade", + Val: "Websocket", + }, + quicpogs.Metadata{ + Key: "HttpHeader:Another-Header", + Val: "Misc", + }, + quicpogs.Metadata{ + Key: "HttpHost", + Val: "cf.host", + }, + quicpogs.Metadata{ + Key: "HttpMethod", + Val: "get", + }, + }, + }, + req: &http.Request{ + Method: "get", + URL: &url.URL{ + Scheme: "http", + Host: "test.com", + }, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{ + "Another-Header": []string{"Misc"}, + }, + ContentLength: 0, + Host: "cf.host", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req, err := buildHTTPRequest(test.connectRequest, nil) + assert.NoError(t, err) + test.req = test.req.WithContext(req.Context()) + assert.Equal(t, test.req, req) + }) + } +} + func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWriteAcker, tcpRequest *TCPRequest) error { rwa.AckConnection() io.Copy(rwa, rwa)