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 })