github.com/quic-go/quic-go@v0.44.0/http3/headers_test.go (about)

     1  package http3
     2  
     3  import (
     4  	"net/http"
     5  	"net/url"
     6  
     7  	. "github.com/onsi/ginkgo/v2"
     8  	. "github.com/onsi/gomega"
     9  	"github.com/quic-go/qpack"
    10  )
    11  
    12  var _ = Describe("Request", func() {
    13  	It("populates requests", func() {
    14  		headers := []qpack.HeaderField{
    15  			{Name: ":path", Value: "/foo"},
    16  			{Name: ":authority", Value: "quic.clemente.io"},
    17  			{Name: ":method", Value: "GET"},
    18  			{Name: "content-length", Value: "42"},
    19  		}
    20  		req, err := requestFromHeaders(headers)
    21  		Expect(err).NotTo(HaveOccurred())
    22  		Expect(req.Method).To(Equal("GET"))
    23  		Expect(req.URL.Path).To(Equal("/foo"))
    24  		Expect(req.URL.Host).To(BeEmpty())
    25  		Expect(req.Proto).To(Equal("HTTP/3.0"))
    26  		Expect(req.ProtoMajor).To(Equal(3))
    27  		Expect(req.ProtoMinor).To(BeZero())
    28  		Expect(req.ContentLength).To(Equal(int64(42)))
    29  		Expect(req.Header).To(HaveLen(1))
    30  		Expect(req.Header.Get("Content-Length")).To(Equal("42"))
    31  		Expect(req.Body).To(BeNil())
    32  		Expect(req.Host).To(Equal("quic.clemente.io"))
    33  		Expect(req.RequestURI).To(Equal("/foo"))
    34  	})
    35  
    36  	It("rejects upper-case fields", func() {
    37  		headers := []qpack.HeaderField{
    38  			{Name: ":path", Value: "/foo"},
    39  			{Name: ":authority", Value: "quic.clemente.io"},
    40  			{Name: ":method", Value: "GET"},
    41  			{Name: "Content-Length", Value: "42"},
    42  		}
    43  		_, err := requestFromHeaders(headers)
    44  		Expect(err).To(MatchError("header field is not lower-case: Content-Length"))
    45  	})
    46  
    47  	It("rejects unknown pseudo headers", func() {
    48  		headers := []qpack.HeaderField{
    49  			{Name: ":path", Value: "/foo"},
    50  			{Name: ":authority", Value: "quic.clemente.io"},
    51  			{Name: ":method", Value: "GET"},
    52  			{Name: ":foo", Value: "bar"},
    53  		}
    54  		_, err := requestFromHeaders(headers)
    55  		Expect(err).To(MatchError("unknown pseudo header: :foo"))
    56  	})
    57  
    58  	It("rejects invalid field names", func() {
    59  		headers := []qpack.HeaderField{
    60  			{Name: ":path", Value: "/foo"},
    61  			{Name: ":authority", Value: "quic.clemente.io"},
    62  			{Name: ":method", Value: "GET"},
    63  			{Name: "@", Value: "42"},
    64  		}
    65  		_, err := requestFromHeaders(headers)
    66  		Expect(err).To(MatchError(`invalid header field name: "@"`))
    67  	})
    68  
    69  	It("rejects invalid field values", func() {
    70  		headers := []qpack.HeaderField{
    71  			{Name: ":path", Value: "/foo"},
    72  			{Name: ":authority", Value: "quic.clemente.io"},
    73  			{Name: ":method", Value: "GET"},
    74  			{Name: "content", Value: "\n"},
    75  		}
    76  		_, err := requestFromHeaders(headers)
    77  		Expect(err).To(MatchError(`invalid header field value for content: "\n"`))
    78  	})
    79  
    80  	It("rejects pseudo header fields after regular header fields", func() {
    81  		headers := []qpack.HeaderField{
    82  			{Name: ":path", Value: "/foo"},
    83  			{Name: "content-length", Value: "42"},
    84  			{Name: ":authority", Value: "quic.clemente.io"},
    85  			{Name: ":method", Value: "GET"},
    86  		}
    87  		_, err := requestFromHeaders(headers)
    88  		Expect(err).To(MatchError("received pseudo header :authority after a regular header field"))
    89  	})
    90  
    91  	It("rejects negative Content-Length values", func() {
    92  		headers := []qpack.HeaderField{
    93  			{Name: ":path", Value: "/foo"},
    94  			{Name: ":authority", Value: "quic.clemente.io"},
    95  			{Name: ":method", Value: "GET"},
    96  			{Name: "content-length", Value: "-42"},
    97  		}
    98  		_, err := requestFromHeaders(headers)
    99  		Expect(err).To(HaveOccurred())
   100  		Expect(err.Error()).To(ContainSubstring("invalid content length"))
   101  	})
   102  
   103  	It("rejects multiple Content-Length headers, if they differ", func() {
   104  		headers := []qpack.HeaderField{
   105  			{Name: ":path", Value: "/foo"},
   106  			{Name: ":authority", Value: "quic.clemente.io"},
   107  			{Name: ":method", Value: "GET"},
   108  			{Name: "content-length", Value: "42"},
   109  			{Name: "content-length", Value: "1337"},
   110  		}
   111  		_, err := requestFromHeaders(headers)
   112  		Expect(err).To(MatchError("contradicting content lengths (42 and 1337)"))
   113  	})
   114  
   115  	It("deduplicates multiple Content-Length headers, if they're the same", func() {
   116  		headers := []qpack.HeaderField{
   117  			{Name: ":path", Value: "/foo"},
   118  			{Name: ":authority", Value: "quic.clemente.io"},
   119  			{Name: ":method", Value: "GET"},
   120  			{Name: "content-length", Value: "42"},
   121  			{Name: "content-length", Value: "42"},
   122  		}
   123  		req, err := requestFromHeaders(headers)
   124  		Expect(err).ToNot(HaveOccurred())
   125  		Expect(req.ContentLength).To(Equal(int64(42)))
   126  		Expect(req.Header.Get("Content-Length")).To(Equal("42"))
   127  	})
   128  
   129  	It("rejects pseudo header fields defined for responses", func() {
   130  		headers := []qpack.HeaderField{
   131  			{Name: ":path", Value: "/foo"},
   132  			{Name: ":authority", Value: "quic.clemente.io"},
   133  			{Name: ":method", Value: "GET"},
   134  			{Name: ":status", Value: "404"},
   135  		}
   136  		_, err := requestFromHeaders(headers)
   137  		Expect(err).To(MatchError("invalid request pseudo header: :status"))
   138  	})
   139  
   140  	It("parses path with leading double slashes", func() {
   141  		headers := []qpack.HeaderField{
   142  			{Name: ":path", Value: "//foo"},
   143  			{Name: ":authority", Value: "quic.clemente.io"},
   144  			{Name: ":method", Value: "GET"},
   145  		}
   146  		req, err := requestFromHeaders(headers)
   147  		Expect(err).NotTo(HaveOccurred())
   148  		Expect(req.Header).To(BeEmpty())
   149  		Expect(req.Body).To(BeNil())
   150  		Expect(req.URL.Path).To(Equal("//foo"))
   151  		Expect(req.URL.Host).To(BeEmpty())
   152  		Expect(req.Host).To(Equal("quic.clemente.io"))
   153  		Expect(req.RequestURI).To(Equal("//foo"))
   154  	})
   155  
   156  	It("concatenates the cookie headers", func() {
   157  		headers := []qpack.HeaderField{
   158  			{Name: ":path", Value: "/foo"},
   159  			{Name: ":authority", Value: "quic.clemente.io"},
   160  			{Name: ":method", Value: "GET"},
   161  			{Name: "cookie", Value: "cookie1=foobar1"},
   162  			{Name: "cookie", Value: "cookie2=foobar2"},
   163  		}
   164  		req, err := requestFromHeaders(headers)
   165  		Expect(err).NotTo(HaveOccurred())
   166  		Expect(req.Header).To(Equal(http.Header{
   167  			"Cookie": []string{"cookie1=foobar1; cookie2=foobar2"},
   168  		}))
   169  	})
   170  
   171  	It("handles Other headers", func() {
   172  		headers := []qpack.HeaderField{
   173  			{Name: ":path", Value: "/foo"},
   174  			{Name: ":authority", Value: "quic.clemente.io"},
   175  			{Name: ":method", Value: "GET"},
   176  			{Name: "cache-control", Value: "max-age=0"},
   177  			{Name: "duplicate-header", Value: "1"},
   178  			{Name: "duplicate-header", Value: "2"},
   179  		}
   180  		req, err := requestFromHeaders(headers)
   181  		Expect(err).NotTo(HaveOccurred())
   182  		Expect(req.Header).To(Equal(http.Header{
   183  			"Cache-Control":    []string{"max-age=0"},
   184  			"Duplicate-Header": []string{"1", "2"},
   185  		}))
   186  	})
   187  
   188  	It("errors with missing path", func() {
   189  		headers := []qpack.HeaderField{
   190  			{Name: ":authority", Value: "quic.clemente.io"},
   191  			{Name: ":method", Value: "GET"},
   192  		}
   193  		_, err := requestFromHeaders(headers)
   194  		Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
   195  	})
   196  
   197  	It("errors with missing method", func() {
   198  		headers := []qpack.HeaderField{
   199  			{Name: ":path", Value: "/foo"},
   200  			{Name: ":authority", Value: "quic.clemente.io"},
   201  		}
   202  		_, err := requestFromHeaders(headers)
   203  		Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
   204  	})
   205  
   206  	It("errors with missing authority", func() {
   207  		headers := []qpack.HeaderField{
   208  			{Name: ":path", Value: "/foo"},
   209  			{Name: ":method", Value: "GET"},
   210  		}
   211  		_, err := requestFromHeaders(headers)
   212  		Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
   213  	})
   214  
   215  	It("errors with invalid protocol", func() {
   216  		headers := []qpack.HeaderField{
   217  			{Name: ":path", Value: "/foo"},
   218  			{Name: ":authority", Value: "quic.clemente.io"},
   219  			{Name: ":method", Value: "GET"},
   220  			{Name: ":protocol", Value: "connect-udp"},
   221  		}
   222  		_, err := requestFromHeaders(headers)
   223  		Expect(err).To(MatchError(":protocol must be empty"))
   224  	})
   225  
   226  	Context("regular HTTP CONNECT", func() {
   227  		It("handles CONNECT method", func() {
   228  			headers := []qpack.HeaderField{
   229  				{Name: ":authority", Value: "quic.clemente.io"},
   230  				{Name: ":method", Value: http.MethodConnect},
   231  			}
   232  			req, err := requestFromHeaders(headers)
   233  			Expect(err).NotTo(HaveOccurred())
   234  			Expect(req.Method).To(Equal(http.MethodConnect))
   235  			Expect(req.Proto).To(Equal("HTTP/3.0"))
   236  			Expect(req.RequestURI).To(Equal("quic.clemente.io"))
   237  		})
   238  
   239  		It("errors with missing authority in CONNECT method", func() {
   240  			headers := []qpack.HeaderField{
   241  				{Name: ":method", Value: http.MethodConnect},
   242  			}
   243  			_, err := requestFromHeaders(headers)
   244  			Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
   245  		})
   246  
   247  		It("errors with extra path in CONNECT method", func() {
   248  			headers := []qpack.HeaderField{
   249  				{Name: ":path", Value: "/foo"},
   250  				{Name: ":authority", Value: "quic.clemente.io"},
   251  				{Name: ":method", Value: http.MethodConnect},
   252  			}
   253  			_, err := requestFromHeaders(headers)
   254  			Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
   255  		})
   256  	})
   257  
   258  	Context("Extended CONNECT", func() {
   259  		It("handles Extended CONNECT method", func() {
   260  			headers := []qpack.HeaderField{
   261  				{Name: ":protocol", Value: "webtransport"},
   262  				{Name: ":scheme", Value: "ftp"},
   263  				{Name: ":method", Value: http.MethodConnect},
   264  				{Name: ":authority", Value: "quic.clemente.io"},
   265  				{Name: ":path", Value: "/foo?val=1337"},
   266  			}
   267  			req, err := requestFromHeaders(headers)
   268  			Expect(err).NotTo(HaveOccurred())
   269  			Expect(req.Method).To(Equal(http.MethodConnect))
   270  			Expect(req.Proto).To(Equal("webtransport"))
   271  			Expect(req.URL.String()).To(Equal("ftp://quic.clemente.io/foo?val=1337"))
   272  			Expect(req.URL.Query().Get("val")).To(Equal("1337"))
   273  		})
   274  
   275  		It("errors with missing scheme", func() {
   276  			headers := []qpack.HeaderField{
   277  				{Name: ":protocol", Value: "webtransport"},
   278  				{Name: ":method", Value: http.MethodConnect},
   279  				{Name: ":authority", Value: "quic.clemente.io"},
   280  				{Name: ":path", Value: "/foo"},
   281  			}
   282  			_, err := requestFromHeaders(headers)
   283  			Expect(err).To(MatchError("extended CONNECT: :scheme, :path and :authority must not be empty"))
   284  		})
   285  	})
   286  
   287  	Context("extracting the hostname from a request", func() {
   288  		var url *url.URL
   289  
   290  		BeforeEach(func() {
   291  			var err error
   292  			url, err = url.Parse("https://quic.clemente.io:1337")
   293  			Expect(err).ToNot(HaveOccurred())
   294  		})
   295  
   296  		It("uses URL.Host", func() {
   297  			Expect(hostnameFromURL(url)).To(Equal("quic.clemente.io:1337"))
   298  		})
   299  
   300  		It("returns an empty hostname if nothing is set", func() {
   301  			Expect(hostnameFromURL(nil)).To(BeEmpty())
   302  		})
   303  	})
   304  })
   305  
   306  var _ = Describe("Response", func() {
   307  	It("populates responses", func() {
   308  		headers := []qpack.HeaderField{
   309  			{Name: ":status", Value: "200"},
   310  			{Name: "content-length", Value: "42"},
   311  		}
   312  		rsp, err := responseFromHeaders(headers)
   313  		Expect(err).NotTo(HaveOccurred())
   314  		Expect(rsp.Proto).To(Equal("HTTP/3.0"))
   315  		Expect(rsp.ProtoMajor).To(Equal(3))
   316  		Expect(rsp.ProtoMinor).To(BeZero())
   317  		Expect(rsp.ContentLength).To(Equal(int64(42)))
   318  		Expect(rsp.Header).To(HaveLen(1))
   319  		Expect(rsp.Header.Get("Content-Length")).To(Equal("42"))
   320  		Expect(rsp.Body).To(BeNil())
   321  		Expect(rsp.StatusCode).To(BeEquivalentTo(200))
   322  		Expect(rsp.Status).To(Equal("200 OK"))
   323  	})
   324  
   325  	It("rejects pseudo header fields after regular header fields", func() {
   326  		headers := []qpack.HeaderField{
   327  			{Name: "content-length", Value: "42"},
   328  			{Name: ":status", Value: "200"},
   329  		}
   330  		_, err := responseFromHeaders(headers)
   331  		Expect(err).To(MatchError("received pseudo header :status after a regular header field"))
   332  	})
   333  
   334  	It("rejects response with no status field", func() {
   335  		headers := []qpack.HeaderField{
   336  			{Name: "content-length", Value: "42"},
   337  		}
   338  		_, err := responseFromHeaders(headers)
   339  		Expect(err).To(MatchError("missing status field"))
   340  	})
   341  
   342  	It("rejects invalid status codes", func() {
   343  		headers := []qpack.HeaderField{
   344  			{Name: ":status", Value: "foobar"},
   345  			{Name: "content-length", Value: "42"},
   346  		}
   347  		_, err := responseFromHeaders(headers)
   348  		Expect(err).To(HaveOccurred())
   349  		Expect(err.Error()).To(ContainSubstring("invalid status code"))
   350  	})
   351  
   352  	It("rejects pseudo header fields defined for requests", func() {
   353  		headers := []qpack.HeaderField{
   354  			{Name: ":status", Value: "404"},
   355  			{Name: ":method", Value: "GET"},
   356  		}
   357  		_, err := responseFromHeaders(headers)
   358  		Expect(err).To(MatchError("invalid response pseudo header: :method"))
   359  	})
   360  })