github.com/danielpfeifer02/quic-go-prio-packs@v0.41.0-28/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 req.URL.Host", func() { 297 req := &http.Request{URL: url} 298 Expect(hostnameFromRequest(req)).To(Equal("quic.clemente.io:1337")) 299 }) 300 301 It("uses req.URL.Host even if req.Host is available", func() { 302 req := &http.Request{ 303 Host: "www.example.org", 304 URL: url, 305 } 306 Expect(hostnameFromRequest(req)).To(Equal("quic.clemente.io:1337")) 307 }) 308 309 It("returns an empty hostname if nothing is set", func() { 310 Expect(hostnameFromRequest(&http.Request{})).To(BeEmpty()) 311 }) 312 }) 313 }) 314 315 var _ = Describe("Response", func() { 316 It("populates responses", func() { 317 headers := []qpack.HeaderField{ 318 {Name: ":status", Value: "200"}, 319 {Name: "content-length", Value: "42"}, 320 } 321 rsp, err := responseFromHeaders(headers) 322 Expect(err).NotTo(HaveOccurred()) 323 Expect(rsp.Proto).To(Equal("HTTP/3.0")) 324 Expect(rsp.ProtoMajor).To(Equal(3)) 325 Expect(rsp.ProtoMinor).To(BeZero()) 326 Expect(rsp.ContentLength).To(Equal(int64(42))) 327 Expect(rsp.Header).To(HaveLen(1)) 328 Expect(rsp.Header.Get("Content-Length")).To(Equal("42")) 329 Expect(rsp.Body).To(BeNil()) 330 Expect(rsp.StatusCode).To(BeEquivalentTo(200)) 331 Expect(rsp.Status).To(Equal("200 OK")) 332 }) 333 334 It("rejects pseudo header fields after regular header fields", func() { 335 headers := []qpack.HeaderField{ 336 {Name: "content-length", Value: "42"}, 337 {Name: ":status", Value: "200"}, 338 } 339 _, err := responseFromHeaders(headers) 340 Expect(err).To(MatchError("received pseudo header :status after a regular header field")) 341 }) 342 343 It("rejects response with no status field", func() { 344 headers := []qpack.HeaderField{ 345 {Name: "content-length", Value: "42"}, 346 } 347 _, err := responseFromHeaders(headers) 348 Expect(err).To(MatchError("missing status field")) 349 }) 350 351 It("rejects invalid status codes", func() { 352 headers := []qpack.HeaderField{ 353 {Name: ":status", Value: "foobar"}, 354 {Name: "content-length", Value: "42"}, 355 } 356 _, err := responseFromHeaders(headers) 357 Expect(err).To(HaveOccurred()) 358 Expect(err.Error()).To(ContainSubstring("invalid status code")) 359 }) 360 361 It("rejects pseudo header fields defined for requests", func() { 362 headers := []qpack.HeaderField{ 363 {Name: ":status", Value: "404"}, 364 {Name: ":method", Value: "GET"}, 365 } 366 _, err := responseFromHeaders(headers) 367 Expect(err).To(MatchError("invalid response pseudo header: :method")) 368 }) 369 })