github.com/tumi8/quic-go@v0.37.4-tum/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  	Context("regular HTTP CONNECT", func() {
   216  		It("handles CONNECT method", func() {
   217  			headers := []qpack.HeaderField{
   218  				{Name: ":authority", Value: "quic.clemente.io"},
   219  				{Name: ":method", Value: http.MethodConnect},
   220  			}
   221  			req, err := requestFromHeaders(headers)
   222  			Expect(err).NotTo(HaveOccurred())
   223  			Expect(req.Method).To(Equal(http.MethodConnect))
   224  			Expect(req.RequestURI).To(Equal("quic.clemente.io"))
   225  		})
   226  
   227  		It("errors with missing authority in CONNECT method", func() {
   228  			headers := []qpack.HeaderField{
   229  				{Name: ":method", Value: http.MethodConnect},
   230  			}
   231  			_, err := requestFromHeaders(headers)
   232  			Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
   233  		})
   234  
   235  		It("errors with extra path in CONNECT method", func() {
   236  			headers := []qpack.HeaderField{
   237  				{Name: ":path", Value: "/foo"},
   238  				{Name: ":authority", Value: "quic.clemente.io"},
   239  				{Name: ":method", Value: http.MethodConnect},
   240  			}
   241  			_, err := requestFromHeaders(headers)
   242  			Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
   243  		})
   244  	})
   245  
   246  	Context("Extended CONNECT", func() {
   247  		It("handles Extended CONNECT method", func() {
   248  			headers := []qpack.HeaderField{
   249  				{Name: ":protocol", Value: "webtransport"},
   250  				{Name: ":scheme", Value: "ftp"},
   251  				{Name: ":method", Value: http.MethodConnect},
   252  				{Name: ":authority", Value: "quic.clemente.io"},
   253  				{Name: ":path", Value: "/foo?val=1337"},
   254  			}
   255  			req, err := requestFromHeaders(headers)
   256  			Expect(err).NotTo(HaveOccurred())
   257  			Expect(req.Method).To(Equal(http.MethodConnect))
   258  			Expect(req.Proto).To(Equal("webtransport"))
   259  			Expect(req.URL.String()).To(Equal("ftp://quic.clemente.io/foo?val=1337"))
   260  			Expect(req.URL.Query().Get("val")).To(Equal("1337"))
   261  		})
   262  
   263  		It("errors with missing scheme", func() {
   264  			headers := []qpack.HeaderField{
   265  				{Name: ":protocol", Value: "webtransport"},
   266  				{Name: ":method", Value: http.MethodConnect},
   267  				{Name: ":authority", Value: "quic.clemente.io"},
   268  				{Name: ":path", Value: "/foo"},
   269  			}
   270  			_, err := requestFromHeaders(headers)
   271  			Expect(err).To(MatchError("extended CONNECT: :scheme, :path and :authority must not be empty"))
   272  		})
   273  	})
   274  
   275  	Context("extracting the hostname from a request", func() {
   276  		var url *url.URL
   277  
   278  		BeforeEach(func() {
   279  			var err error
   280  			url, err = url.Parse("https://quic.clemente.io:1337")
   281  			Expect(err).ToNot(HaveOccurred())
   282  		})
   283  
   284  		It("uses req.URL.Host", func() {
   285  			req := &http.Request{URL: url}
   286  			Expect(hostnameFromRequest(req)).To(Equal("quic.clemente.io:1337"))
   287  		})
   288  
   289  		It("uses req.URL.Host even if req.Host is available", func() {
   290  			req := &http.Request{
   291  				Host: "www.example.org",
   292  				URL:  url,
   293  			}
   294  			Expect(hostnameFromRequest(req)).To(Equal("quic.clemente.io:1337"))
   295  		})
   296  
   297  		It("returns an empty hostname if nothing is set", func() {
   298  			Expect(hostnameFromRequest(&http.Request{})).To(BeEmpty())
   299  		})
   300  	})
   301  })
   302  
   303  var _ = Describe("Response", func() {
   304  	It("populates responses", func() {
   305  		headers := []qpack.HeaderField{
   306  			{Name: ":status", Value: "200"},
   307  			{Name: "content-length", Value: "42"},
   308  		}
   309  		rsp, err := responseFromHeaders(headers)
   310  		Expect(err).NotTo(HaveOccurred())
   311  		Expect(rsp.Proto).To(Equal("HTTP/3.0"))
   312  		Expect(rsp.ProtoMajor).To(Equal(3))
   313  		Expect(rsp.ProtoMinor).To(BeZero())
   314  		Expect(rsp.ContentLength).To(Equal(int64(42)))
   315  		Expect(rsp.Header).To(HaveLen(1))
   316  		Expect(rsp.Header.Get("Content-Length")).To(Equal("42"))
   317  		Expect(rsp.Body).To(BeNil())
   318  		Expect(rsp.StatusCode).To(BeEquivalentTo(200))
   319  		Expect(rsp.Status).To(Equal("200 OK"))
   320  	})
   321  
   322  	It("rejects pseudo header fields after regular header fields", func() {
   323  		headers := []qpack.HeaderField{
   324  			{Name: "content-length", Value: "42"},
   325  			{Name: ":status", Value: "200"},
   326  		}
   327  		_, err := responseFromHeaders(headers)
   328  		Expect(err).To(MatchError("received pseudo header :status after a regular header field"))
   329  	})
   330  
   331  	It("rejects response with no status field", func() {
   332  		headers := []qpack.HeaderField{
   333  			{Name: "content-length", Value: "42"},
   334  		}
   335  		_, err := responseFromHeaders(headers)
   336  		Expect(err).To(MatchError("missing status field"))
   337  	})
   338  
   339  	It("rejects invalid status codes", func() {
   340  		headers := []qpack.HeaderField{
   341  			{Name: ":status", Value: "foobar"},
   342  			{Name: "content-length", Value: "42"},
   343  		}
   344  		_, err := responseFromHeaders(headers)
   345  		Expect(err).To(HaveOccurred())
   346  		Expect(err.Error()).To(ContainSubstring("invalid status code"))
   347  	})
   348  
   349  	It("rejects pseudo header fields defined for requests", func() {
   350  		headers := []qpack.HeaderField{
   351  			{Name: ":status", Value: "404"},
   352  			{Name: ":method", Value: "GET"},
   353  		}
   354  		_, err := responseFromHeaders(headers)
   355  		Expect(err).To(MatchError("invalid response pseudo header: :method"))
   356  	})
   357  })