From d1498c360e207522c7f45c3fb30c39b66139b008 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Sat, 2 Apr 2022 19:55:42 +0100 Subject: [PATCH] add support for serializing Extended CONNECT requests (#3360) --- http3/request_writer.go | 14 +++++++++--- http3/request_writer_test.go | 43 +++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/http3/request_writer.go b/http3/request_writer.go index 8878c8f19f7..aebb640b17e 100644 --- a/http3/request_writer.go +++ b/http3/request_writer.go @@ -113,7 +113,9 @@ func (w *requestWriter) writeHeaders(wr io.Writer, req *http.Request, gzip bool) } // copied from net/transport.go - +// Modified to support Extended CONNECT: +// Contrary to what the godoc for the http.Request says, +// we do respect the Proto field if the method is CONNECT. func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) error { host := req.Host if host == "" { @@ -124,8 +126,11 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra return err } + // http.NewRequest sets this field to HTTP/1.1 + isExtendedConnect := req.Method == http.MethodConnect && req.Proto != "" && req.Proto != "HTTP/1.1" + var path string - if req.Method != "CONNECT" { + if req.Method != http.MethodConnect || isExtendedConnect { path = req.URL.RequestURI() if !validPseudoPath(path) { orig := path @@ -162,10 +167,13 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra // [RFC3986]). f(":authority", host) f(":method", req.Method) - if req.Method != "CONNECT" { + if req.Method != http.MethodConnect || isExtendedConnect { f(":path", path) f(":scheme", req.URL.Scheme) } + if isExtendedConnect { + f(":protocol", req.Proto) + } if trailers != "" { f("trailer", trailers) } diff --git a/http3/request_writer_test.go b/http3/request_writer_test.go index 83c77204bda..95adc951021 100644 --- a/http3/request_writer_test.go +++ b/http3/request_writer_test.go @@ -6,12 +6,12 @@ import ( "net/http" "strconv" - "github.com/marten-seemann/qpack" - - "github.com/golang/mock/gomock" mockquic "github.com/lucas-clemente/quic-go/internal/mocks/quic" "github.com/lucas-clemente/quic-go/internal/utils" + "github.com/golang/mock/gomock" + "github.com/marten-seemann/qpack" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -58,7 +58,7 @@ var _ = Describe("Request Writer", func() { It("writes a GET request", func() { str.EXPECT().Close() - req, err := http.NewRequest("GET", "https://quic.clemente.io/index.html?foo=bar", nil) + req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/index.html?foo=bar", nil) Expect(err).ToNot(HaveOccurred()) Expect(rw.WriteRequest(str, req, false)).To(Succeed()) headerFields := decode(strBuf) @@ -73,7 +73,7 @@ var _ = Describe("Request Writer", func() { closed := make(chan struct{}) str.EXPECT().Close().Do(func() { close(closed) }) postData := bytes.NewReader([]byte("foobar")) - req, err := http.NewRequest("POST", "https://quic.clemente.io/upload.html", postData) + req, err := http.NewRequest(http.MethodPost, "https://quic.clemente.io/upload.html", postData) Expect(err).ToNot(HaveOccurred()) Expect(rw.WriteRequest(str, req, false)).To(Succeed()) @@ -94,7 +94,7 @@ var _ = Describe("Request Writer", func() { It("writes a POST request, if the Body returns an EOF immediately", func() { closed := make(chan struct{}) str.EXPECT().Close().Do(func() { close(closed) }) - req, err := http.NewRequest("POST", "https://quic.clemente.io/upload.html", &foobarReader{}) + req, err := http.NewRequest(http.MethodPost, "https://quic.clemente.io/upload.html", &foobarReader{}) Expect(err).ToNot(HaveOccurred()) Expect(rw.WriteRequest(str, req, false)).To(Succeed()) @@ -110,7 +110,7 @@ var _ = Describe("Request Writer", func() { It("sends cookies", func() { str.EXPECT().Close() - req, err := http.NewRequest("GET", "https://quic.clemente.io/", nil) + req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/", nil) Expect(err).ToNot(HaveOccurred()) cookie1 := &http.Cookie{ Name: "Cookie #1", @@ -129,10 +129,37 @@ var _ = Describe("Request Writer", func() { It("adds the header for gzip support", func() { str.EXPECT().Close() - req, err := http.NewRequest("GET", "https://quic.clemente.io/", nil) + req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/", nil) Expect(err).ToNot(HaveOccurred()) Expect(rw.WriteRequest(str, req, true)).To(Succeed()) headerFields := decode(strBuf) Expect(headerFields).To(HaveKeyWithValue("accept-encoding", "gzip")) }) + + It("writes a CONNECT request", func() { + str.EXPECT().Close() + req, err := http.NewRequest(http.MethodConnect, "https://quic.clemente.io/", nil) + Expect(err).ToNot(HaveOccurred()) + Expect(rw.WriteRequest(str, req, false)).To(Succeed()) + headerFields := decode(strBuf) + Expect(headerFields).To(HaveKeyWithValue(":method", "CONNECT")) + Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io")) + Expect(headerFields).ToNot(HaveKey(":path")) + Expect(headerFields).ToNot(HaveKey(":scheme")) + Expect(headerFields).ToNot(HaveKey(":protocol")) + }) + + It("writes an Extended CONNECT request", func() { + str.EXPECT().Close() + req, err := http.NewRequest(http.MethodConnect, "https://quic.clemente.io/foobar", nil) + Expect(err).ToNot(HaveOccurred()) + req.Proto = "webtransport" + Expect(rw.WriteRequest(str, req, false)).To(Succeed()) + headerFields := decode(strBuf) + Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io")) + Expect(headerFields).To(HaveKeyWithValue(":method", "CONNECT")) + Expect(headerFields).To(HaveKeyWithValue(":path", "/foobar")) + Expect(headerFields).To(HaveKeyWithValue(":scheme", "https")) + Expect(headerFields).To(HaveKeyWithValue(":protocol", "webtransport")) + }) })