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