diff --git a/http3/request.go b/http3/request.go index b5fc5d5aca7..e98ab7fac0b 100644 --- a/http3/request.go +++ b/http3/request.go @@ -12,9 +12,9 @@ import ( ) func requestFromHeaders(headers []qpack.HeaderField) (*http.Request, error) { - var path, authority, method, contentLengthStr string - httpHeaders := http.Header{} + var path, authority, method, protocol, scheme, contentLengthStr string + httpHeaders := http.Header{} for _, h := range headers { switch h.Name { case ":path": @@ -23,6 +23,10 @@ func requestFromHeaders(headers []qpack.HeaderField) (*http.Request, error) { method = h.Value case ":authority": authority = h.Value + case ":protocol": + protocol = h.Value + case ":scheme": + scheme = h.Value case "content-length": contentLengthStr = h.Value default: @@ -39,7 +43,12 @@ func requestFromHeaders(headers []qpack.HeaderField) (*http.Request, error) { isConnect := method == http.MethodConnect if isConnect { - if path != "" || authority == "" { + // Extended CONNECT, see https://datatracker.ietf.org/doc/html/rfc8441#section-4 + if protocol != "" { + if scheme == "" || path == "" || authority == "" { + return nil, errors.New("extended CONNECT: :scheme, :path and :authority must not be empty") + } + } else if path != "" || authority == "" { // normal CONNECT return nil, errors.New(":path must be empty and :authority must not be empty") } } else if len(path) == 0 || len(authority) == 0 || len(method) == 0 { @@ -51,9 +60,14 @@ func requestFromHeaders(headers []qpack.HeaderField) (*http.Request, error) { var err error if isConnect { - u = &url.URL{Host: authority} + u = &url.URL{ + Scheme: scheme, + Host: authority, + Path: path, + } requestURI = authority } else { + protocol = "HTTP/3" u, err = url.ParseRequestURI(path) if err != nil { return nil, err @@ -72,7 +86,7 @@ func requestFromHeaders(headers []qpack.HeaderField) (*http.Request, error) { return &http.Request{ Method: method, URL: u, - Proto: "HTTP/3", + Proto: protocol, ProtoMajor: 3, ProtoMinor: 0, Header: httpHeaders, diff --git a/http3/request_test.go b/http3/request_test.go index edaba2a59b4..e430dc4e877 100644 --- a/http3/request_test.go +++ b/http3/request_test.go @@ -81,17 +81,6 @@ var _ = Describe("Request", func() { })) }) - It("handles CONNECT method", func() { - headers := []qpack.HeaderField{ - {Name: ":authority", Value: "quic.clemente.io"}, - {Name: ":method", Value: http.MethodConnect}, - } - req, err := requestFromHeaders(headers) - Expect(err).NotTo(HaveOccurred()) - Expect(req.Method).To(Equal(http.MethodConnect)) - Expect(req.RequestURI).To(Equal("quic.clemente.io")) - }) - It("errors with missing path", func() { headers := []qpack.HeaderField{ {Name: ":authority", Value: "quic.clemente.io"}, @@ -119,22 +108,63 @@ var _ = Describe("Request", func() { Expect(err).To(MatchError(":path, :authority and :method must not be empty")) }) - It("errors with missing authority in CONNECT method", func() { - headers := []qpack.HeaderField{ - {Name: ":method", Value: http.MethodConnect}, - } - _, err := requestFromHeaders(headers) - Expect(err).To(MatchError(":path must be empty and :authority must not be empty")) + Context("regular HTTP CONNECT", func() { + It("handles CONNECT method", func() { + headers := []qpack.HeaderField{ + {Name: ":authority", Value: "quic.clemente.io"}, + {Name: ":method", Value: http.MethodConnect}, + } + req, err := requestFromHeaders(headers) + Expect(err).NotTo(HaveOccurred()) + Expect(req.Method).To(Equal(http.MethodConnect)) + Expect(req.RequestURI).To(Equal("quic.clemente.io")) + }) + + It("errors with missing authority in CONNECT method", func() { + headers := []qpack.HeaderField{ + {Name: ":method", Value: http.MethodConnect}, + } + _, err := requestFromHeaders(headers) + Expect(err).To(MatchError(":path must be empty and :authority must not be empty")) + }) + + It("errors with extra path in CONNECT method", func() { + headers := []qpack.HeaderField{ + {Name: ":path", Value: "/foo"}, + {Name: ":authority", Value: "quic.clemente.io"}, + {Name: ":method", Value: http.MethodConnect}, + } + _, err := requestFromHeaders(headers) + Expect(err).To(MatchError(":path must be empty and :authority must not be empty")) + }) }) - It("errors with extra path in CONNECT method", func() { - headers := []qpack.HeaderField{ - {Name: ":path", Value: "/foo"}, - {Name: ":authority", Value: "quic.clemente.io"}, - {Name: ":method", Value: http.MethodConnect}, - } - _, err := requestFromHeaders(headers) - Expect(err).To(MatchError(":path must be empty and :authority must not be empty")) + Context("Extended CONNECT", func() { + It("handles Extended CONNECT method", func() { + headers := []qpack.HeaderField{ + {Name: ":protocol", Value: "webtransport"}, + {Name: ":scheme", Value: "ftp"}, + {Name: ":method", Value: http.MethodConnect}, + {Name: ":authority", Value: "quic.clemente.io"}, + {Name: ":path", Value: "/foo"}, + } + req, err := requestFromHeaders(headers) + Expect(err).NotTo(HaveOccurred()) + Expect(req.Method).To(Equal(http.MethodConnect)) + Expect(req.Proto).To(Equal("webtransport")) + Expect(req.URL.String()).To(Equal("ftp://quic.clemente.io/foo")) + }) + + It("errors with missing scheme", func() { + headers := []qpack.HeaderField{ + {Name: ":protocol", Value: "webtransport"}, + {Name: ":method", Value: http.MethodConnect}, + {Name: ":authority", Value: "quic.clemente.io"}, + {Name: ":path", Value: "/foo"}, + } + _, err := requestFromHeaders(headers) + Expect(err).To(MatchError("extended CONNECT: :scheme, :path and :authority must not be empty")) + }) }) Context("extracting the hostname from a request", func() {