cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2021.12.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

connection/h2mux_header_test.go

642lines · modecode

1package connection
2
3import (
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
20type ByName []h2mux.Header
21
22func (a ByName) Len() int { return len(a) }
23func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
24func (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
32func 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
47func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
48 return []h2mux.Header{{
49 Name: headersField,
50 Value: SerializeHeaders(headers),
51 }}
52}
53
54func 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
71func 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
91func 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
111func 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
131func 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
293func 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
365func 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`.
375func 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()
393func 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()
402func 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()
409func 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)
418func 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
429func 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
439func 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
469func 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
491func 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
510func 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
521func 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
535func 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
583func 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
611func 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
621func 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