cloudflare/cloudflared
Publicmirrored from https://github.com/cloudflare/cloudflaredAvailable
connection/h2mux_header_test.go
642lines · modecode
| 1 | package connection |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "math/rand" |
| 6 | "net/http" |
| 7 | "net/url" |
| 8 | "reflect" |
| 9 | "regexp" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "testing/quick" |
| 13 | |
| 14 | "github.com/stretchr/testify/assert" |
| 15 | "github.com/stretchr/testify/require" |
| 16 | |
| 17 | "github.com/cloudflare/cloudflared/h2mux" |
| 18 | ) |
| 19 | |
| 20 | type ByName []h2mux.Header |
| 21 | |
| 22 | func (a ByName) Len() int { return len(a) } |
| 23 | func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| 24 | func (a ByName) Less(i, j int) bool { |
| 25 | if a[i].Name == a[j].Name { |
| 26 | return a[i].Value < a[j].Value |
| 27 | } |
| 28 | |
| 29 | return a[i].Name < a[j].Name |
| 30 | } |
| 31 | |
| 32 | func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) { |
| 33 | request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) |
| 34 | assert.NoError(t, err) |
| 35 | |
| 36 | mockHeaders := http.Header{ |
| 37 | "Mock header 1": {"Mock value 1"}, |
| 38 | "Mock header 2": {"Mock value 2"}, |
| 39 | } |
| 40 | |
| 41 | headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request) |
| 42 | |
| 43 | assert.True(t, reflect.DeepEqual(mockHeaders, request.Header)) |
| 44 | assert.NoError(t, headersConversionErr) |
| 45 | } |
| 46 | |
| 47 | func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header { |
| 48 | return []h2mux.Header{{ |
| 49 | Name: headersField, |
| 50 | Value: SerializeHeaders(headers), |
| 51 | }} |
| 52 | } |
| 53 | |
| 54 | func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) { |
| 55 | request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) |
| 56 | assert.NoError(t, err) |
| 57 | |
| 58 | emptyHeaders := make(http.Header) |
| 59 | headersConversionErr := H2RequestHeadersToH1Request( |
| 60 | []h2mux.Header{{ |
| 61 | Name: RequestUserHeaders, |
| 62 | Value: SerializeHeaders(emptyHeaders), |
| 63 | }}, |
| 64 | request, |
| 65 | ) |
| 66 | |
| 67 | assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header)) |
| 68 | assert.NoError(t, headersConversionErr) |
| 69 | } |
| 70 | |
| 71 | func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) { |
| 72 | request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) |
| 73 | assert.NoError(t, err) |
| 74 | |
| 75 | mockRequestHeaders := []h2mux.Header{ |
| 76 | {Name: ":path", Value: "//bad_path/"}, |
| 77 | {Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})}, |
| 78 | } |
| 79 | |
| 80 | headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request) |
| 81 | |
| 82 | assert.Equal(t, http.Header{ |
| 83 | "Mock header": []string{"Mock value"}, |
| 84 | }, request.Header) |
| 85 | |
| 86 | assert.Equal(t, "http://example.com//bad_path/", request.URL.String()) |
| 87 | |
| 88 | assert.NoError(t, headersConversionErr) |
| 89 | } |
| 90 | |
| 91 | func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) { |
| 92 | request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) |
| 93 | assert.NoError(t, err) |
| 94 | |
| 95 | mockRequestHeaders := []h2mux.Header{ |
| 96 | {Name: ":path", Value: "/?query=mock%20value"}, |
| 97 | {Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})}, |
| 98 | } |
| 99 | |
| 100 | headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request) |
| 101 | |
| 102 | assert.Equal(t, http.Header{ |
| 103 | "Mock header": []string{"Mock value"}, |
| 104 | }, request.Header) |
| 105 | |
| 106 | assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String()) |
| 107 | |
| 108 | assert.NoError(t, headersConversionErr) |
| 109 | } |
| 110 | |
| 111 | func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) { |
| 112 | request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) |
| 113 | assert.NoError(t, err) |
| 114 | |
| 115 | mockRequestHeaders := []h2mux.Header{ |
| 116 | {Name: ":path", Value: "/mock%20path"}, |
| 117 | {Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})}, |
| 118 | } |
| 119 | |
| 120 | headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request) |
| 121 | |
| 122 | assert.Equal(t, http.Header{ |
| 123 | "Mock header": []string{"Mock value"}, |
| 124 | }, request.Header) |
| 125 | |
| 126 | assert.Equal(t, "http://example.com/mock%20path", request.URL.String()) |
| 127 | |
| 128 | assert.NoError(t, headersConversionErr) |
| 129 | } |
| 130 | |
| 131 | func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) { |
| 132 | type testCase struct { |
| 133 | path string |
| 134 | want string |
| 135 | } |
| 136 | testCases := []testCase{ |
| 137 | { |
| 138 | path: "", |
| 139 | want: "", |
| 140 | }, |
| 141 | { |
| 142 | path: "/", |
| 143 | want: "/", |
| 144 | }, |
| 145 | { |
| 146 | path: "//", |
| 147 | want: "//", |
| 148 | }, |
| 149 | { |
| 150 | path: "/test", |
| 151 | want: "/test", |
| 152 | }, |
| 153 | { |
| 154 | path: "//test", |
| 155 | want: "//test", |
| 156 | }, |
| 157 | { |
| 158 | // https://github.com/cloudflare/cloudflared/issues/81 |
| 159 | path: "//test/", |
| 160 | want: "//test/", |
| 161 | }, |
| 162 | { |
| 163 | path: "/%2Ftest", |
| 164 | want: "/%2Ftest", |
| 165 | }, |
| 166 | { |
| 167 | path: "//%20test", |
| 168 | want: "//%20test", |
| 169 | }, |
| 170 | { |
| 171 | // https://github.com/cloudflare/cloudflared/issues/124 |
| 172 | path: "/test?get=somthing%20a", |
| 173 | want: "/test?get=somthing%20a", |
| 174 | }, |
| 175 | { |
| 176 | path: "/%20", |
| 177 | want: "/%20", |
| 178 | }, |
| 179 | { |
| 180 | // stdlib's EscapedPath() will always percent-encode ' ' |
| 181 | path: "/ ", |
| 182 | want: "/%20", |
| 183 | }, |
| 184 | { |
| 185 | path: "/ a ", |
| 186 | want: "/%20a%20", |
| 187 | }, |
| 188 | { |
| 189 | path: "/a%20b", |
| 190 | want: "/a%20b", |
| 191 | }, |
| 192 | { |
| 193 | path: "/foo/bar;param?query#frag", |
| 194 | want: "/foo/bar;param?query#frag", |
| 195 | }, |
| 196 | { |
| 197 | // stdlib's EscapedPath() will always percent-encode non-ASCII chars |
| 198 | path: "/a␠b", |
| 199 | want: "/a%E2%90%A0b", |
| 200 | }, |
| 201 | { |
| 202 | path: "/a-umlaut-ä", |
| 203 | want: "/a-umlaut-%C3%A4", |
| 204 | }, |
| 205 | { |
| 206 | path: "/a-umlaut-%C3%A4", |
| 207 | want: "/a-umlaut-%C3%A4", |
| 208 | }, |
| 209 | { |
| 210 | path: "/a-umlaut-%c3%a4", |
| 211 | want: "/a-umlaut-%c3%a4", |
| 212 | }, |
| 213 | { |
| 214 | // here the second '#' is treated as part of the fragment |
| 215 | path: "/a#b#c", |
| 216 | want: "/a#b%23c", |
| 217 | }, |
| 218 | { |
| 219 | path: "/a#b␠c", |
| 220 | want: "/a#b%E2%90%A0c", |
| 221 | }, |
| 222 | { |
| 223 | path: "/a#b%20c", |
| 224 | want: "/a#b%20c", |
| 225 | }, |
| 226 | { |
| 227 | path: "/a#b c", |
| 228 | want: "/a#b%20c", |
| 229 | }, |
| 230 | { |
| 231 | // stdlib's EscapedPath() will always percent-encode '\' |
| 232 | path: "/\\", |
| 233 | want: "/%5C", |
| 234 | }, |
| 235 | { |
| 236 | path: "/a\\", |
| 237 | want: "/a%5C", |
| 238 | }, |
| 239 | { |
| 240 | path: "/a,b.c.", |
| 241 | want: "/a,b.c.", |
| 242 | }, |
| 243 | { |
| 244 | path: "/.", |
| 245 | want: "/.", |
| 246 | }, |
| 247 | { |
| 248 | // stdlib's EscapedPath() will always percent-encode '`' |
| 249 | path: "/a`", |
| 250 | want: "/a%60", |
| 251 | }, |
| 252 | { |
| 253 | path: "/a[0]", |
| 254 | want: "/a[0]", |
| 255 | }, |
| 256 | { |
| 257 | path: "/?a[0]=5 &b[]=", |
| 258 | want: "/?a[0]=5 &b[]=", |
| 259 | }, |
| 260 | { |
| 261 | path: "/?a=%22b%20%22", |
| 262 | want: "/?a=%22b%20%22", |
| 263 | }, |
| 264 | } |
| 265 | |
| 266 | for index, testCase := range testCases { |
| 267 | requestURL := "https://example.com" |
| 268 | |
| 269 | request, err := http.NewRequest(http.MethodGet, requestURL, nil) |
| 270 | assert.NoError(t, err) |
| 271 | |
| 272 | mockRequestHeaders := []h2mux.Header{ |
| 273 | {Name: ":path", Value: testCase.path}, |
| 274 | {Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})}, |
| 275 | } |
| 276 | |
| 277 | headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request) |
| 278 | assert.NoError(t, headersConversionErr) |
| 279 | |
| 280 | assert.Equal(t, |
| 281 | http.Header{ |
| 282 | "Mock header": []string{"Mock value"}, |
| 283 | }, |
| 284 | request.Header) |
| 285 | |
| 286 | assert.Equal(t, |
| 287 | "https://example.com"+testCase.want, |
| 288 | request.URL.String(), |
| 289 | "Failed URL index: %v %#v", index, testCase) |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) { |
| 294 | config := &quick.Config{ |
| 295 | Values: func(args []reflect.Value, rand *rand.Rand) { |
| 296 | args[0] = reflect.ValueOf(randomHTTP2Path(t, rand)) |
| 297 | }, |
| 298 | } |
| 299 | |
| 300 | type testOrigin struct { |
| 301 | url string |
| 302 | |
| 303 | expectedScheme string |
| 304 | expectedBasePath string |
| 305 | } |
| 306 | testOrigins := []testOrigin{ |
| 307 | { |
| 308 | url: "http://origin.hostname.example.com:8080", |
| 309 | expectedScheme: "http", |
| 310 | expectedBasePath: "http://origin.hostname.example.com:8080", |
| 311 | }, |
| 312 | { |
| 313 | url: "http://origin.hostname.example.com:8080/", |
| 314 | expectedScheme: "http", |
| 315 | expectedBasePath: "http://origin.hostname.example.com:8080", |
| 316 | }, |
| 317 | { |
| 318 | url: "http://origin.hostname.example.com:8080/api", |
| 319 | expectedScheme: "http", |
| 320 | expectedBasePath: "http://origin.hostname.example.com:8080/api", |
| 321 | }, |
| 322 | { |
| 323 | url: "http://origin.hostname.example.com:8080/api/", |
| 324 | expectedScheme: "http", |
| 325 | expectedBasePath: "http://origin.hostname.example.com:8080/api", |
| 326 | }, |
| 327 | { |
| 328 | url: "https://origin.hostname.example.com:8080/api", |
| 329 | expectedScheme: "https", |
| 330 | expectedBasePath: "https://origin.hostname.example.com:8080/api", |
| 331 | }, |
| 332 | } |
| 333 | |
| 334 | // use multiple schemes to demonstrate that the URL is based on the |
| 335 | // origin's scheme, not the :scheme header |
| 336 | for _, testScheme := range []string{"http", "https"} { |
| 337 | for _, testOrigin := range testOrigins { |
| 338 | assertion := func(testPath string) bool { |
| 339 | const expectedMethod = "POST" |
| 340 | const expectedHostname = "request.hostname.example.com" |
| 341 | |
| 342 | h2 := []h2mux.Header{ |
| 343 | {Name: ":method", Value: expectedMethod}, |
| 344 | {Name: ":scheme", Value: testScheme}, |
| 345 | {Name: ":authority", Value: expectedHostname}, |
| 346 | {Name: ":path", Value: testPath}, |
| 347 | {Name: RequestUserHeaders, Value: ""}, |
| 348 | } |
| 349 | h1, err := http.NewRequest("GET", testOrigin.url, nil) |
| 350 | require.NoError(t, err) |
| 351 | |
| 352 | err = H2RequestHeadersToH1Request(h2, h1) |
| 353 | return assert.NoError(t, err) && |
| 354 | assert.Equal(t, expectedMethod, h1.Method) && |
| 355 | assert.Equal(t, expectedHostname, h1.Host) && |
| 356 | assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) && |
| 357 | assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String()) |
| 358 | } |
| 359 | err := quick.Check(assertion, config) |
| 360 | assert.NoError(t, err) |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | func randomASCIIPrintableChar(rand *rand.Rand) int { |
| 366 | // smallest printable ASCII char is 32, largest is 126 |
| 367 | const startPrintable = 32 |
| 368 | const endPrintable = 127 |
| 369 | return startPrintable + rand.Intn(endPrintable-startPrintable) |
| 370 | } |
| 371 | |
| 372 | // randomASCIIText generates an ASCII string, some of whose characters may be |
| 373 | // percent-encoded. Its "logical length" (ignoring percent-encoding) is |
| 374 | // between 1 and `maxLength`. |
| 375 | func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string { |
| 376 | length := minLength + rand.Intn(maxLength) |
| 377 | var result strings.Builder |
| 378 | for i := 0; i < length; i++ { |
| 379 | c := randomASCIIPrintableChar(rand) |
| 380 | |
| 381 | // 1/4 chance of using percent encoding when not necessary |
| 382 | if c == '%' || rand.Intn(4) == 0 { |
| 383 | result.WriteString(fmt.Sprintf("%%%02X", c)) |
| 384 | } else { |
| 385 | result.WriteByte(byte(c)) |
| 386 | } |
| 387 | } |
| 388 | return result.String() |
| 389 | } |
| 390 | |
| 391 | // Calls `randomASCIIText` and ensures the result is a valid URL path, |
| 392 | // i.e. one that can pass unchanged through url.URL.String() |
| 393 | func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string { |
| 394 | text := randomASCIIText(rand, minLength, maxLength) |
| 395 | re, err := regexp.Compile("[^/;,]*") |
| 396 | require.NoError(t, err) |
| 397 | return "/" + re.ReplaceAllStringFunc(text, url.PathEscape) |
| 398 | } |
| 399 | |
| 400 | // Calls `randomASCIIText` and ensures the result is a valid URL query, |
| 401 | // i.e. one that can pass unchanged through url.URL.String() |
| 402 | func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string { |
| 403 | text := randomASCIIText(rand, minLength, maxLength) |
| 404 | return "?" + strings.ReplaceAll(text, "#", "%23") |
| 405 | } |
| 406 | |
| 407 | // Calls `randomASCIIText` and ensures the result is a valid URL fragment, |
| 408 | // i.e. one that can pass unchanged through url.URL.String() |
| 409 | func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string { |
| 410 | text := randomASCIIText(rand, minLength, maxLength) |
| 411 | u, err := url.Parse("#" + text) |
| 412 | require.NoError(t, err) |
| 413 | return u.String() |
| 414 | } |
| 415 | |
| 416 | // Assemble a random :path pseudoheader that is legal by Go stdlib standards |
| 417 | // (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations) |
| 418 | func randomHTTP2Path(t *testing.T, rand *rand.Rand) string { |
| 419 | result := randomHTTP1Path(t, rand, 1, 64) |
| 420 | if rand.Intn(2) == 1 { |
| 421 | result += randomHTTP1Query(rand, 1, 32) |
| 422 | } |
| 423 | if rand.Intn(2) == 1 { |
| 424 | result += randomHTTP1Fragment(t, rand, 1, 16) |
| 425 | } |
| 426 | return result |
| 427 | } |
| 428 | |
| 429 | func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) { |
| 430 | for name, values := range headers { |
| 431 | for _, value := range values { |
| 432 | h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value}) |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | return h2muxHeaders |
| 437 | } |
| 438 | |
| 439 | func TestParseRequestHeaders(t *testing.T) { |
| 440 | mockUserHeadersToSerialize := http.Header{ |
| 441 | "Mock-Header-One": {"1", "1.5"}, |
| 442 | "Mock-Header-Two": {"2"}, |
| 443 | "Mock-Header-Three": {"3"}, |
| 444 | } |
| 445 | |
| 446 | mockHeaders := []h2mux.Header{ |
| 447 | {Name: "One", Value: "1"}, // will be dropped |
| 448 | {Name: "Cf-Two", Value: "cf-value-1"}, |
| 449 | {Name: "Cf-Two", Value: "cf-value-2"}, |
| 450 | {Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)}, |
| 451 | } |
| 452 | |
| 453 | expectedHeaders := []h2mux.Header{ |
| 454 | {Name: "Cf-Two", Value: "cf-value-1"}, |
| 455 | {Name: "Cf-Two", Value: "cf-value-2"}, |
| 456 | {Name: "Mock-Header-One", Value: "1"}, |
| 457 | {Name: "Mock-Header-One", Value: "1.5"}, |
| 458 | {Name: "Mock-Header-Two", Value: "2"}, |
| 459 | {Name: "Mock-Header-Three", Value: "3"}, |
| 460 | } |
| 461 | h1 := &http.Request{ |
| 462 | Header: make(http.Header), |
| 463 | } |
| 464 | err := H2RequestHeadersToH1Request(mockHeaders, h1) |
| 465 | assert.NoError(t, err) |
| 466 | assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header)) |
| 467 | } |
| 468 | |
| 469 | func TestIsH2muxControlRequestHeader(t *testing.T) { |
| 470 | controlRequestHeaders := []string{ |
| 471 | // Anything that begins with cf- |
| 472 | "cf-sample-header", |
| 473 | |
| 474 | // Any http2 pseudoheader |
| 475 | ":sample-pseudo-header", |
| 476 | |
| 477 | // content-length is a special case, it has to be there |
| 478 | // for some requests to work (per the HTTP2 spec) |
| 479 | "content-length", |
| 480 | |
| 481 | // Websocket request headers |
| 482 | "connection", |
| 483 | "upgrade", |
| 484 | } |
| 485 | |
| 486 | for _, header := range controlRequestHeaders { |
| 487 | assert.True(t, IsH2muxControlRequestHeader(header)) |
| 488 | } |
| 489 | } |
| 490 | |
| 491 | func TestIsH2muxControlResponseHeader(t *testing.T) { |
| 492 | controlResponseHeaders := []string{ |
| 493 | // Anything that begins with cf-int- or cf-cloudflared- |
| 494 | "cf-int-sample-header", |
| 495 | "cf-cloudflared-sample-header", |
| 496 | |
| 497 | // Any http2 pseudoheader |
| 498 | ":sample-pseudo-header", |
| 499 | |
| 500 | // content-length is a special case, it has to be there |
| 501 | // for some requests to work (per the HTTP2 spec) |
| 502 | "content-length", |
| 503 | } |
| 504 | |
| 505 | for _, header := range controlResponseHeaders { |
| 506 | assert.True(t, IsH2muxControlResponseHeader(header)) |
| 507 | } |
| 508 | } |
| 509 | |
| 510 | func TestIsNotH2muxControlRequestHeader(t *testing.T) { |
| 511 | notControlRequestHeaders := []string{ |
| 512 | "mock-header", |
| 513 | "another-sample-header", |
| 514 | } |
| 515 | |
| 516 | for _, header := range notControlRequestHeaders { |
| 517 | assert.False(t, IsH2muxControlRequestHeader(header)) |
| 518 | } |
| 519 | } |
| 520 | |
| 521 | func TestIsNotH2muxControlResponseHeader(t *testing.T) { |
| 522 | notControlResponseHeaders := []string{ |
| 523 | "mock-header", |
| 524 | "another-sample-header", |
| 525 | "upgrade", |
| 526 | "connection", |
| 527 | "cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared- |
| 528 | } |
| 529 | |
| 530 | for _, header := range notControlResponseHeaders { |
| 531 | assert.False(t, IsH2muxControlResponseHeader(header)) |
| 532 | } |
| 533 | } |
| 534 | |
| 535 | func TestH1ResponseToH2ResponseHeaders(t *testing.T) { |
| 536 | mockHeaders := http.Header{ |
| 537 | "User-header-one": {""}, |
| 538 | "User-header-two": {"1", "2"}, |
| 539 | "cf-header": {"cf-value"}, |
| 540 | "cf-int-header": {"cf-int-value"}, |
| 541 | "cf-cloudflared-header": {"cf-cloudflared-value"}, |
| 542 | "Content-Length": {"123"}, |
| 543 | } |
| 544 | mockResponse := http.Response{ |
| 545 | StatusCode: 200, |
| 546 | Header: mockHeaders, |
| 547 | } |
| 548 | |
| 549 | headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header) |
| 550 | |
| 551 | serializedHeadersIndex := -1 |
| 552 | for i, header := range headers { |
| 553 | if header.Name == ResponseUserHeaders { |
| 554 | serializedHeadersIndex = i |
| 555 | break |
| 556 | } |
| 557 | } |
| 558 | assert.NotEqual(t, -1, serializedHeadersIndex) |
| 559 | actualControlHeaders := append( |
| 560 | headers[:serializedHeadersIndex], |
| 561 | headers[serializedHeadersIndex+1:]..., |
| 562 | ) |
| 563 | expectedControlHeaders := []h2mux.Header{ |
| 564 | {Name: ":status", Value: "200"}, |
| 565 | {Name: "content-length", Value: "123"}, |
| 566 | } |
| 567 | |
| 568 | assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders) |
| 569 | |
| 570 | actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value) |
| 571 | expectedUserHeaders := []h2mux.Header{ |
| 572 | {Name: "User-header-one", Value: ""}, |
| 573 | {Name: "User-header-two", Value: "1"}, |
| 574 | {Name: "User-header-two", Value: "2"}, |
| 575 | {Name: "cf-header", Value: "cf-value"}, |
| 576 | } |
| 577 | assert.NoError(t, err) |
| 578 | assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders) |
| 579 | } |
| 580 | |
| 581 | // The purpose of this test is to check that our code and the http.Header |
| 582 | // implementation don't throw validation errors about header size |
| 583 | func TestHeaderSize(t *testing.T) { |
| 584 | largeValue := randSeq(5 * 1024 * 1024) // 5Mb |
| 585 | largeHeaders := http.Header{ |
| 586 | "User-header": {largeValue}, |
| 587 | } |
| 588 | mockResponse := http.Response{ |
| 589 | StatusCode: 200, |
| 590 | Header: largeHeaders, |
| 591 | } |
| 592 | |
| 593 | serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header) |
| 594 | request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil) |
| 595 | assert.NoError(t, err) |
| 596 | for _, header := range serializedHeaders { |
| 597 | request.Header.Set(header.Name, header.Value) |
| 598 | } |
| 599 | |
| 600 | for _, header := range serializedHeaders { |
| 601 | if header.Name != ResponseUserHeaders { |
| 602 | continue |
| 603 | } |
| 604 | |
| 605 | deserializedHeaders, err := DeserializeHeaders(header.Value) |
| 606 | assert.NoError(t, err) |
| 607 | assert.Equal(t, largeValue, deserializedHeaders[0].Value) |
| 608 | } |
| 609 | } |
| 610 | |
| 611 | func randSeq(n int) string { |
| 612 | randomizer := rand.New(rand.NewSource(17)) |
| 613 | var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") |
| 614 | b := make([]rune, n) |
| 615 | for i := range b { |
| 616 | b[i] = letters[randomizer.Intn(len(letters))] |
| 617 | } |
| 618 | return string(b) |
| 619 | } |
| 620 | |
| 621 | func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) { |
| 622 | ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ" |
| 623 | h2, _ := DeserializeHeaders(ser) |
| 624 | h1 := make(http.Header) |
| 625 | for _, header := range h2 { |
| 626 | h1.Add(header.Name, header.Value) |
| 627 | } |
| 628 | h1.Add("Content-Length", "200") |
| 629 | h1.Add("Cf-Something", "Else") |
| 630 | h1.Add("Upgrade", "websocket") |
| 631 | |
| 632 | h1resp := &http.Response{ |
| 633 | StatusCode: 200, |
| 634 | Header: h1, |
| 635 | } |
| 636 | |
| 637 | b.ReportAllocs() |
| 638 | b.ResetTimer() |
| 639 | for i := 0; i < b.N; i++ { |
| 640 | _ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header) |
| 641 | } |
| 642 | } |
| 643 | |