TUN-5393: Content-length is no longer a control header for non-h2mux transports
- Refactors some h2mux specific logic from connection/header.go to connection/h2mux_header.go - Do the same for the unit tests - Add a non-h2mux "is control response header" function (we don't need one for the request flow) - In that new function, do not consider "content-length" as a control header - Use that function in the non-h2mux flow for response (and it will be used also in origintunneld)
This commit is contained in:
		
							parent
							
								
									a96d4243ba
								
							
						
					
					
						commit
						e35f744b36
					
				| 
						 | 
					@ -0,0 +1,128 @@
 | 
				
			||||||
 | 
					package connection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cloudflare/cloudflared/h2mux"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
 | 
				
			||||||
 | 
					// to an HTTP/1 Request object destined for the local origin web service.
 | 
				
			||||||
 | 
					// This operation includes conversion of the pseudo-headers into their closest
 | 
				
			||||||
 | 
					// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
 | 
				
			||||||
 | 
					func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
 | 
				
			||||||
 | 
						for _, header := range h2 {
 | 
				
			||||||
 | 
							name := strings.ToLower(header.Name)
 | 
				
			||||||
 | 
							if !IsH2muxControlRequestHeader(name) {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch name {
 | 
				
			||||||
 | 
							case ":method":
 | 
				
			||||||
 | 
								h1.Method = header.Value
 | 
				
			||||||
 | 
							case ":scheme":
 | 
				
			||||||
 | 
								// noop - use the preexisting scheme from h1.URL
 | 
				
			||||||
 | 
							case ":authority":
 | 
				
			||||||
 | 
								// Otherwise the host header will be based on the origin URL
 | 
				
			||||||
 | 
								h1.Host = header.Value
 | 
				
			||||||
 | 
							case ":path":
 | 
				
			||||||
 | 
								// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
 | 
				
			||||||
 | 
								// However, this HTTP/1 Request object belongs to the Go standard library,
 | 
				
			||||||
 | 
								// whose URL package makes some opinionated decisions about the encoding of
 | 
				
			||||||
 | 
								// URL characters: see the docs of https://godoc.org/net/url#URL,
 | 
				
			||||||
 | 
								// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
 | 
				
			||||||
 | 
								// which is always used when computing url.URL.String(), whether we'd like it or not.
 | 
				
			||||||
 | 
								//
 | 
				
			||||||
 | 
								// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
 | 
				
			||||||
 | 
								// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
 | 
				
			||||||
 | 
								// is treated differently when HTTP_PROXY is set!
 | 
				
			||||||
 | 
								// See https://github.com/golang/go/issues/5684#issuecomment-66080888
 | 
				
			||||||
 | 
								//
 | 
				
			||||||
 | 
								// This means we are subject to the behavior of net/url's function `shouldEscape`
 | 
				
			||||||
 | 
								// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if header.Value == "*" {
 | 
				
			||||||
 | 
									h1.URL.Path = "*"
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// Due to the behavior of validation.ValidateUrl, h1.URL may
 | 
				
			||||||
 | 
								// already have a partial value, with or without a trailing slash.
 | 
				
			||||||
 | 
								base := h1.URL.String()
 | 
				
			||||||
 | 
								base = strings.TrimRight(base, "/")
 | 
				
			||||||
 | 
								// But we know :path begins with '/', because we handled '*' above - see RFC7540
 | 
				
			||||||
 | 
								requestURL, err := url.Parse(base + header.Value)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								h1.URL = requestURL
 | 
				
			||||||
 | 
							case "content-length":
 | 
				
			||||||
 | 
								contentLength, err := strconv.ParseInt(header.Value, 10, 64)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("unparseable content length")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								h1.ContentLength = contentLength
 | 
				
			||||||
 | 
							case RequestUserHeaders:
 | 
				
			||||||
 | 
								// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
 | 
				
			||||||
 | 
								// Find and parse user headers serialized into a single one
 | 
				
			||||||
 | 
								userHeaders, err := DeserializeHeaders(header.Value)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errors.Wrap(err, "Unable to parse user headers")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, userHeader := range userHeaders {
 | 
				
			||||||
 | 
									h1.Header.Add(userHeader.Name, userHeader.Value)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								// All other control headers shall just be proxied transparently
 | 
				
			||||||
 | 
								h1.Header.Add(header.Name, header.Value)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
 | 
				
			||||||
 | 
						h2 = []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: ":status", Value: strconv.Itoa(status)},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						userHeaders := make(http.Header, len(h1))
 | 
				
			||||||
 | 
						for header, values := range h1 {
 | 
				
			||||||
 | 
							h2name := strings.ToLower(header)
 | 
				
			||||||
 | 
							if h2name == "content-length" {
 | 
				
			||||||
 | 
								// This header has meaning in HTTP/2 and will be used by the edge,
 | 
				
			||||||
 | 
								// so it should be sent as an HTTP/2 response header.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Since these are http2 headers, they're required to be lowercase
 | 
				
			||||||
 | 
								h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
 | 
				
			||||||
 | 
							} else if !IsH2muxControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
 | 
				
			||||||
 | 
								// User headers, on the other hand, must all be serialized so that
 | 
				
			||||||
 | 
								// HTTP/2 header validation won't be applied to HTTP/1 header values
 | 
				
			||||||
 | 
								userHeaders[header] = values
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Perform user header serialization and set them in the single header
 | 
				
			||||||
 | 
						h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
 | 
				
			||||||
 | 
						return h2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsH2muxControlRequestHeader is called in the direction of eyeball -> origin.
 | 
				
			||||||
 | 
					func IsH2muxControlRequestHeader(headerName string) bool {
 | 
				
			||||||
 | 
						return headerName == "content-length" ||
 | 
				
			||||||
 | 
							headerName == "connection" || headerName == "upgrade" || // Websocket request headers
 | 
				
			||||||
 | 
							strings.HasPrefix(headerName, ":") ||
 | 
				
			||||||
 | 
							strings.HasPrefix(headerName, "cf-")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsH2muxControlResponseHeader is called in the direction of eyeball <- origin.
 | 
				
			||||||
 | 
					func IsH2muxControlResponseHeader(headerName string) bool {
 | 
				
			||||||
 | 
						return headerName == "content-length" ||
 | 
				
			||||||
 | 
							strings.HasPrefix(headerName, ":") ||
 | 
				
			||||||
 | 
							strings.HasPrefix(headerName, "cf-int-") ||
 | 
				
			||||||
 | 
							strings.HasPrefix(headerName, "cf-cloudflared-")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,642 @@
 | 
				
			||||||
 | 
					package connection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"math/rand"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"testing/quick"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cloudflare/cloudflared/h2mux"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ByName []h2mux.Header
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a ByName) Len() int      { return len(a) }
 | 
				
			||||||
 | 
					func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
 | 
				
			||||||
 | 
					func (a ByName) Less(i, j int) bool {
 | 
				
			||||||
 | 
						if a[i].Name == a[j].Name {
 | 
				
			||||||
 | 
							return a[i].Value < a[j].Value
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return a[i].Name < a[j].Name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
 | 
				
			||||||
 | 
						request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockHeaders := http.Header{
 | 
				
			||||||
 | 
							"Mock header 1": {"Mock value 1"},
 | 
				
			||||||
 | 
							"Mock header 2": {"Mock value 2"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
 | 
				
			||||||
 | 
						assert.NoError(t, headersConversionErr)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
 | 
				
			||||||
 | 
						return []h2mux.Header{{
 | 
				
			||||||
 | 
							Name:  headersField,
 | 
				
			||||||
 | 
							Value: SerializeHeaders(headers),
 | 
				
			||||||
 | 
						}}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
 | 
				
			||||||
 | 
						request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						emptyHeaders := make(http.Header)
 | 
				
			||||||
 | 
						headersConversionErr := H2RequestHeadersToH1Request(
 | 
				
			||||||
 | 
							[]h2mux.Header{{
 | 
				
			||||||
 | 
								Name:  RequestUserHeaders,
 | 
				
			||||||
 | 
								Value: SerializeHeaders(emptyHeaders),
 | 
				
			||||||
 | 
							}},
 | 
				
			||||||
 | 
							request,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
 | 
				
			||||||
 | 
						assert.NoError(t, headersConversionErr)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
 | 
				
			||||||
 | 
						request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockRequestHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: ":path", Value: "//bad_path/"},
 | 
				
			||||||
 | 
							{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, http.Header{
 | 
				
			||||||
 | 
							"Mock header": []string{"Mock value"},
 | 
				
			||||||
 | 
						}, request.Header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, headersConversionErr)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
 | 
				
			||||||
 | 
						request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockRequestHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: ":path", Value: "/?query=mock%20value"},
 | 
				
			||||||
 | 
							{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, http.Header{
 | 
				
			||||||
 | 
							"Mock header": []string{"Mock value"},
 | 
				
			||||||
 | 
						}, request.Header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, headersConversionErr)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
 | 
				
			||||||
 | 
						request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockRequestHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: ":path", Value: "/mock%20path"},
 | 
				
			||||||
 | 
							{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, http.Header{
 | 
				
			||||||
 | 
							"Mock header": []string{"Mock value"},
 | 
				
			||||||
 | 
						}, request.Header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, headersConversionErr)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
 | 
				
			||||||
 | 
						type testCase struct {
 | 
				
			||||||
 | 
							path string
 | 
				
			||||||
 | 
							want string
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						testCases := []testCase{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "",
 | 
				
			||||||
 | 
								want: "",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/",
 | 
				
			||||||
 | 
								want: "/",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "//",
 | 
				
			||||||
 | 
								want: "//",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/test",
 | 
				
			||||||
 | 
								want: "/test",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "//test",
 | 
				
			||||||
 | 
								want: "//test",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// https://github.com/cloudflare/cloudflared/issues/81
 | 
				
			||||||
 | 
								path: "//test/",
 | 
				
			||||||
 | 
								want: "//test/",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/%2Ftest",
 | 
				
			||||||
 | 
								want: "/%2Ftest",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "//%20test",
 | 
				
			||||||
 | 
								want: "//%20test",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// https://github.com/cloudflare/cloudflared/issues/124
 | 
				
			||||||
 | 
								path: "/test?get=somthing%20a",
 | 
				
			||||||
 | 
								want: "/test?get=somthing%20a",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/%20",
 | 
				
			||||||
 | 
								want: "/%20",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// stdlib's EscapedPath() will always percent-encode ' '
 | 
				
			||||||
 | 
								path: "/ ",
 | 
				
			||||||
 | 
								want: "/%20",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/ a ",
 | 
				
			||||||
 | 
								want: "/%20a%20",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a%20b",
 | 
				
			||||||
 | 
								want: "/a%20b",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/foo/bar;param?query#frag",
 | 
				
			||||||
 | 
								want: "/foo/bar;param?query#frag",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// stdlib's EscapedPath() will always percent-encode non-ASCII chars
 | 
				
			||||||
 | 
								path: "/a␠b",
 | 
				
			||||||
 | 
								want: "/a%E2%90%A0b",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a-umlaut-ä",
 | 
				
			||||||
 | 
								want: "/a-umlaut-%C3%A4",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a-umlaut-%C3%A4",
 | 
				
			||||||
 | 
								want: "/a-umlaut-%C3%A4",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a-umlaut-%c3%a4",
 | 
				
			||||||
 | 
								want: "/a-umlaut-%c3%a4",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// here the second '#' is treated as part of the fragment
 | 
				
			||||||
 | 
								path: "/a#b#c",
 | 
				
			||||||
 | 
								want: "/a#b%23c",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a#b␠c",
 | 
				
			||||||
 | 
								want: "/a#b%E2%90%A0c",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a#b%20c",
 | 
				
			||||||
 | 
								want: "/a#b%20c",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a#b c",
 | 
				
			||||||
 | 
								want: "/a#b%20c",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// stdlib's EscapedPath() will always percent-encode '\'
 | 
				
			||||||
 | 
								path: "/\\",
 | 
				
			||||||
 | 
								want: "/%5C",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a\\",
 | 
				
			||||||
 | 
								want: "/a%5C",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a,b.c.",
 | 
				
			||||||
 | 
								want: "/a,b.c.",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/.",
 | 
				
			||||||
 | 
								want: "/.",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// stdlib's EscapedPath() will always percent-encode '`'
 | 
				
			||||||
 | 
								path: "/a`",
 | 
				
			||||||
 | 
								want: "/a%60",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/a[0]",
 | 
				
			||||||
 | 
								want: "/a[0]",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/?a[0]=5 &b[]=",
 | 
				
			||||||
 | 
								want: "/?a[0]=5 &b[]=",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: "/?a=%22b%20%22",
 | 
				
			||||||
 | 
								want: "/?a=%22b%20%22",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for index, testCase := range testCases {
 | 
				
			||||||
 | 
							requestURL := "https://example.com"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							request, err := http.NewRequest(http.MethodGet, requestURL, nil)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							mockRequestHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
								{Name: ":path", Value: testCase.path},
 | 
				
			||||||
 | 
								{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
				
			||||||
 | 
							assert.NoError(t, headersConversionErr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t,
 | 
				
			||||||
 | 
								http.Header{
 | 
				
			||||||
 | 
									"Mock header": []string{"Mock value"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								request.Header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t,
 | 
				
			||||||
 | 
								"https://example.com"+testCase.want,
 | 
				
			||||||
 | 
								request.URL.String(),
 | 
				
			||||||
 | 
								"Failed URL index: %v %#v", index, testCase)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
 | 
				
			||||||
 | 
						config := &quick.Config{
 | 
				
			||||||
 | 
							Values: func(args []reflect.Value, rand *rand.Rand) {
 | 
				
			||||||
 | 
								args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type testOrigin struct {
 | 
				
			||||||
 | 
							url string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							expectedScheme   string
 | 
				
			||||||
 | 
							expectedBasePath string
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						testOrigins := []testOrigin{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								url:              "http://origin.hostname.example.com:8080",
 | 
				
			||||||
 | 
								expectedScheme:   "http",
 | 
				
			||||||
 | 
								expectedBasePath: "http://origin.hostname.example.com:8080",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								url:              "http://origin.hostname.example.com:8080/",
 | 
				
			||||||
 | 
								expectedScheme:   "http",
 | 
				
			||||||
 | 
								expectedBasePath: "http://origin.hostname.example.com:8080",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								url:              "http://origin.hostname.example.com:8080/api",
 | 
				
			||||||
 | 
								expectedScheme:   "http",
 | 
				
			||||||
 | 
								expectedBasePath: "http://origin.hostname.example.com:8080/api",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								url:              "http://origin.hostname.example.com:8080/api/",
 | 
				
			||||||
 | 
								expectedScheme:   "http",
 | 
				
			||||||
 | 
								expectedBasePath: "http://origin.hostname.example.com:8080/api",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								url:              "https://origin.hostname.example.com:8080/api",
 | 
				
			||||||
 | 
								expectedScheme:   "https",
 | 
				
			||||||
 | 
								expectedBasePath: "https://origin.hostname.example.com:8080/api",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// use multiple schemes to demonstrate that the URL is based on the
 | 
				
			||||||
 | 
						// origin's scheme, not the :scheme header
 | 
				
			||||||
 | 
						for _, testScheme := range []string{"http", "https"} {
 | 
				
			||||||
 | 
							for _, testOrigin := range testOrigins {
 | 
				
			||||||
 | 
								assertion := func(testPath string) bool {
 | 
				
			||||||
 | 
									const expectedMethod = "POST"
 | 
				
			||||||
 | 
									const expectedHostname = "request.hostname.example.com"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									h2 := []h2mux.Header{
 | 
				
			||||||
 | 
										{Name: ":method", Value: expectedMethod},
 | 
				
			||||||
 | 
										{Name: ":scheme", Value: testScheme},
 | 
				
			||||||
 | 
										{Name: ":authority", Value: expectedHostname},
 | 
				
			||||||
 | 
										{Name: ":path", Value: testPath},
 | 
				
			||||||
 | 
										{Name: RequestUserHeaders, Value: ""},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									h1, err := http.NewRequest("GET", testOrigin.url, nil)
 | 
				
			||||||
 | 
									require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									err = H2RequestHeadersToH1Request(h2, h1)
 | 
				
			||||||
 | 
									return assert.NoError(t, err) &&
 | 
				
			||||||
 | 
										assert.Equal(t, expectedMethod, h1.Method) &&
 | 
				
			||||||
 | 
										assert.Equal(t, expectedHostname, h1.Host) &&
 | 
				
			||||||
 | 
										assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
 | 
				
			||||||
 | 
										assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err := quick.Check(assertion, config)
 | 
				
			||||||
 | 
								assert.NoError(t, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func randomASCIIPrintableChar(rand *rand.Rand) int {
 | 
				
			||||||
 | 
						// smallest printable ASCII char is 32, largest is 126
 | 
				
			||||||
 | 
						const startPrintable = 32
 | 
				
			||||||
 | 
						const endPrintable = 127
 | 
				
			||||||
 | 
						return startPrintable + rand.Intn(endPrintable-startPrintable)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// randomASCIIText generates an ASCII string, some of whose characters may be
 | 
				
			||||||
 | 
					// percent-encoded. Its "logical length" (ignoring percent-encoding) is
 | 
				
			||||||
 | 
					// between 1 and `maxLength`.
 | 
				
			||||||
 | 
					func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
 | 
				
			||||||
 | 
						length := minLength + rand.Intn(maxLength)
 | 
				
			||||||
 | 
						var result strings.Builder
 | 
				
			||||||
 | 
						for i := 0; i < length; i++ {
 | 
				
			||||||
 | 
							c := randomASCIIPrintableChar(rand)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 1/4 chance of using percent encoding when not necessary
 | 
				
			||||||
 | 
							if c == '%' || rand.Intn(4) == 0 {
 | 
				
			||||||
 | 
								result.WriteString(fmt.Sprintf("%%%02X", c))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								result.WriteByte(byte(c))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Calls `randomASCIIText` and ensures the result is a valid URL path,
 | 
				
			||||||
 | 
					// i.e. one that can pass unchanged through url.URL.String()
 | 
				
			||||||
 | 
					func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
 | 
				
			||||||
 | 
						text := randomASCIIText(rand, minLength, maxLength)
 | 
				
			||||||
 | 
						re, err := regexp.Compile("[^/;,]*")
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Calls `randomASCIIText` and ensures the result is a valid URL query,
 | 
				
			||||||
 | 
					// i.e. one that can pass unchanged through url.URL.String()
 | 
				
			||||||
 | 
					func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
 | 
				
			||||||
 | 
						text := randomASCIIText(rand, minLength, maxLength)
 | 
				
			||||||
 | 
						return "?" + strings.ReplaceAll(text, "#", "%23")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
 | 
				
			||||||
 | 
					// i.e. one that can pass unchanged through url.URL.String()
 | 
				
			||||||
 | 
					func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
 | 
				
			||||||
 | 
						text := randomASCIIText(rand, minLength, maxLength)
 | 
				
			||||||
 | 
						u, err := url.Parse("#" + text)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						return u.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Assemble a random :path pseudoheader that is legal by Go stdlib standards
 | 
				
			||||||
 | 
					// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
 | 
				
			||||||
 | 
					func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
 | 
				
			||||||
 | 
						result := randomHTTP1Path(t, rand, 1, 64)
 | 
				
			||||||
 | 
						if rand.Intn(2) == 1 {
 | 
				
			||||||
 | 
							result += randomHTTP1Query(rand, 1, 32)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rand.Intn(2) == 1 {
 | 
				
			||||||
 | 
							result += randomHTTP1Fragment(t, rand, 1, 16)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
 | 
				
			||||||
 | 
						for name, values := range headers {
 | 
				
			||||||
 | 
							for _, value := range values {
 | 
				
			||||||
 | 
								h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return h2muxHeaders
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseRequestHeaders(t *testing.T) {
 | 
				
			||||||
 | 
						mockUserHeadersToSerialize := http.Header{
 | 
				
			||||||
 | 
							"Mock-Header-One":   {"1", "1.5"},
 | 
				
			||||||
 | 
							"Mock-Header-Two":   {"2"},
 | 
				
			||||||
 | 
							"Mock-Header-Three": {"3"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: "One", Value: "1"}, // will be dropped
 | 
				
			||||||
 | 
							{Name: "Cf-Two", Value: "cf-value-1"},
 | 
				
			||||||
 | 
							{Name: "Cf-Two", Value: "cf-value-2"},
 | 
				
			||||||
 | 
							{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						expectedHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: "Cf-Two", Value: "cf-value-1"},
 | 
				
			||||||
 | 
							{Name: "Cf-Two", Value: "cf-value-2"},
 | 
				
			||||||
 | 
							{Name: "Mock-Header-One", Value: "1"},
 | 
				
			||||||
 | 
							{Name: "Mock-Header-One", Value: "1.5"},
 | 
				
			||||||
 | 
							{Name: "Mock-Header-Two", Value: "2"},
 | 
				
			||||||
 | 
							{Name: "Mock-Header-Three", Value: "3"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h1 := &http.Request{
 | 
				
			||||||
 | 
							Header: make(http.Header),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := H2RequestHeadersToH1Request(mockHeaders, h1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsH2muxControlRequestHeader(t *testing.T) {
 | 
				
			||||||
 | 
						controlRequestHeaders := []string{
 | 
				
			||||||
 | 
							// Anything that begins with cf-
 | 
				
			||||||
 | 
							"cf-sample-header",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Any http2 pseudoheader
 | 
				
			||||||
 | 
							":sample-pseudo-header",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// content-length is a special case, it has to be there
 | 
				
			||||||
 | 
							// for some requests to work (per the HTTP2 spec)
 | 
				
			||||||
 | 
							"content-length",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Websocket request headers
 | 
				
			||||||
 | 
							"connection",
 | 
				
			||||||
 | 
							"upgrade",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, header := range controlRequestHeaders {
 | 
				
			||||||
 | 
							assert.True(t, IsH2muxControlRequestHeader(header))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsH2muxControlResponseHeader(t *testing.T) {
 | 
				
			||||||
 | 
						controlResponseHeaders := []string{
 | 
				
			||||||
 | 
							// Anything that begins with cf-int- or cf-cloudflared-
 | 
				
			||||||
 | 
							"cf-int-sample-header",
 | 
				
			||||||
 | 
							"cf-cloudflared-sample-header",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Any http2 pseudoheader
 | 
				
			||||||
 | 
							":sample-pseudo-header",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// content-length is a special case, it has to be there
 | 
				
			||||||
 | 
							// for some requests to work (per the HTTP2 spec)
 | 
				
			||||||
 | 
							"content-length",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, header := range controlResponseHeaders {
 | 
				
			||||||
 | 
							assert.True(t, IsH2muxControlResponseHeader(header))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsNotH2muxControlRequestHeader(t *testing.T) {
 | 
				
			||||||
 | 
						notControlRequestHeaders := []string{
 | 
				
			||||||
 | 
							"mock-header",
 | 
				
			||||||
 | 
							"another-sample-header",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, header := range notControlRequestHeaders {
 | 
				
			||||||
 | 
							assert.False(t, IsH2muxControlRequestHeader(header))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIsNotH2muxControlResponseHeader(t *testing.T) {
 | 
				
			||||||
 | 
						notControlResponseHeaders := []string{
 | 
				
			||||||
 | 
							"mock-header",
 | 
				
			||||||
 | 
							"another-sample-header",
 | 
				
			||||||
 | 
							"upgrade",
 | 
				
			||||||
 | 
							"connection",
 | 
				
			||||||
 | 
							"cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared-
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, header := range notControlResponseHeaders {
 | 
				
			||||||
 | 
							assert.False(t, IsH2muxControlResponseHeader(header))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
 | 
				
			||||||
 | 
						mockHeaders := http.Header{
 | 
				
			||||||
 | 
							"User-header-one":       {""},
 | 
				
			||||||
 | 
							"User-header-two":       {"1", "2"},
 | 
				
			||||||
 | 
							"cf-header":             {"cf-value"},
 | 
				
			||||||
 | 
							"cf-int-header":         {"cf-int-value"},
 | 
				
			||||||
 | 
							"cf-cloudflared-header": {"cf-cloudflared-value"},
 | 
				
			||||||
 | 
							"Content-Length":        {"123"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mockResponse := http.Response{
 | 
				
			||||||
 | 
							StatusCode: 200,
 | 
				
			||||||
 | 
							Header:     mockHeaders,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						serializedHeadersIndex := -1
 | 
				
			||||||
 | 
						for i, header := range headers {
 | 
				
			||||||
 | 
							if header.Name == ResponseUserHeaders {
 | 
				
			||||||
 | 
								serializedHeadersIndex = i
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						assert.NotEqual(t, -1, serializedHeadersIndex)
 | 
				
			||||||
 | 
						actualControlHeaders := append(
 | 
				
			||||||
 | 
							headers[:serializedHeadersIndex],
 | 
				
			||||||
 | 
							headers[serializedHeadersIndex+1:]...,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						expectedControlHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: ":status", Value: "200"},
 | 
				
			||||||
 | 
							{Name: "content-length", Value: "123"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
 | 
				
			||||||
 | 
						expectedUserHeaders := []h2mux.Header{
 | 
				
			||||||
 | 
							{Name: "User-header-one", Value: ""},
 | 
				
			||||||
 | 
							{Name: "User-header-two", Value: "1"},
 | 
				
			||||||
 | 
							{Name: "User-header-two", Value: "2"},
 | 
				
			||||||
 | 
							{Name: "cf-header", Value: "cf-value"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The purpose of this test is to check that our code and the http.Header
 | 
				
			||||||
 | 
					// implementation don't throw validation errors about header size
 | 
				
			||||||
 | 
					func TestHeaderSize(t *testing.T) {
 | 
				
			||||||
 | 
						largeValue := randSeq(5 * 1024 * 1024) // 5Mb
 | 
				
			||||||
 | 
						largeHeaders := http.Header{
 | 
				
			||||||
 | 
							"User-header": {largeValue},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mockResponse := http.Response{
 | 
				
			||||||
 | 
							StatusCode: 200,
 | 
				
			||||||
 | 
							Header:     largeHeaders,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
 | 
				
			||||||
 | 
						request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						for _, header := range serializedHeaders {
 | 
				
			||||||
 | 
							request.Header.Set(header.Name, header.Value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, header := range serializedHeaders {
 | 
				
			||||||
 | 
							if header.Name != ResponseUserHeaders {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							deserializedHeaders, err := DeserializeHeaders(header.Value)
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.Equal(t, largeValue, deserializedHeaders[0].Value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func randSeq(n int) string {
 | 
				
			||||||
 | 
						randomizer := rand.New(rand.NewSource(17))
 | 
				
			||||||
 | 
						var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
 | 
				
			||||||
 | 
						b := make([]rune, n)
 | 
				
			||||||
 | 
						for i := range b {
 | 
				
			||||||
 | 
							b[i] = letters[randomizer.Intn(len(letters))]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return string(b)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
 | 
				
			||||||
 | 
						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"
 | 
				
			||||||
 | 
						h2, _ := DeserializeHeaders(ser)
 | 
				
			||||||
 | 
						h1 := make(http.Header)
 | 
				
			||||||
 | 
						for _, header := range h2 {
 | 
				
			||||||
 | 
							h1.Add(header.Name, header.Value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h1.Add("Content-Length", "200")
 | 
				
			||||||
 | 
						h1.Add("Cf-Something", "Else")
 | 
				
			||||||
 | 
						h1.Add("Upgrade", "websocket")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h1resp := &http.Response{
 | 
				
			||||||
 | 
							StatusCode: 200,
 | 
				
			||||||
 | 
							Header:     h1,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.ReportAllocs()
 | 
				
			||||||
 | 
						b.ResetTimer()
 | 
				
			||||||
 | 
						for i := 0; i < b.N; i++ {
 | 
				
			||||||
 | 
							_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,8 +4,6 @@ import (
 | 
				
			||||||
	"encoding/base64"
 | 
						"encoding/base64"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/pkg/errors"
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
| 
						 | 
					@ -44,93 +42,9 @@ func mustInitRespMetaHeader(src string) string {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var headerEncoding = base64.RawStdEncoding
 | 
					var headerEncoding = base64.RawStdEncoding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// note: all h2mux headers should be lower-case (http/2 style)
 | 
					// IsControlResponseHeader is called in the direction of eyeball <- origin.
 | 
				
			||||||
const ()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
 | 
					 | 
				
			||||||
// to an HTTP/1 Request object destined for the local origin web service.
 | 
					 | 
				
			||||||
// This operation includes conversion of the pseudo-headers into their closest
 | 
					 | 
				
			||||||
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
 | 
					 | 
				
			||||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
 | 
					 | 
				
			||||||
	for _, header := range h2 {
 | 
					 | 
				
			||||||
		name := strings.ToLower(header.Name)
 | 
					 | 
				
			||||||
		if !IsControlRequestHeader(name) {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		switch name {
 | 
					 | 
				
			||||||
		case ":method":
 | 
					 | 
				
			||||||
			h1.Method = header.Value
 | 
					 | 
				
			||||||
		case ":scheme":
 | 
					 | 
				
			||||||
			// noop - use the preexisting scheme from h1.URL
 | 
					 | 
				
			||||||
		case ":authority":
 | 
					 | 
				
			||||||
			// Otherwise the host header will be based on the origin URL
 | 
					 | 
				
			||||||
			h1.Host = header.Value
 | 
					 | 
				
			||||||
		case ":path":
 | 
					 | 
				
			||||||
			// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
 | 
					 | 
				
			||||||
			// However, this HTTP/1 Request object belongs to the Go standard library,
 | 
					 | 
				
			||||||
			// whose URL package makes some opinionated decisions about the encoding of
 | 
					 | 
				
			||||||
			// URL characters: see the docs of https://godoc.org/net/url#URL,
 | 
					 | 
				
			||||||
			// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
 | 
					 | 
				
			||||||
			// which is always used when computing url.URL.String(), whether we'd like it or not.
 | 
					 | 
				
			||||||
			//
 | 
					 | 
				
			||||||
			// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
 | 
					 | 
				
			||||||
			// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
 | 
					 | 
				
			||||||
			// is treated differently when HTTP_PROXY is set!
 | 
					 | 
				
			||||||
			// See https://github.com/golang/go/issues/5684#issuecomment-66080888
 | 
					 | 
				
			||||||
			//
 | 
					 | 
				
			||||||
			// This means we are subject to the behavior of net/url's function `shouldEscape`
 | 
					 | 
				
			||||||
			// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if header.Value == "*" {
 | 
					 | 
				
			||||||
				h1.URL.Path = "*"
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			// Due to the behavior of validation.ValidateUrl, h1.URL may
 | 
					 | 
				
			||||||
			// already have a partial value, with or without a trailing slash.
 | 
					 | 
				
			||||||
			base := h1.URL.String()
 | 
					 | 
				
			||||||
			base = strings.TrimRight(base, "/")
 | 
					 | 
				
			||||||
			// But we know :path begins with '/', because we handled '*' above - see RFC7540
 | 
					 | 
				
			||||||
			requestURL, err := url.Parse(base + header.Value)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			h1.URL = requestURL
 | 
					 | 
				
			||||||
		case "content-length":
 | 
					 | 
				
			||||||
			contentLength, err := strconv.ParseInt(header.Value, 10, 64)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return fmt.Errorf("unparseable content length")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			h1.ContentLength = contentLength
 | 
					 | 
				
			||||||
		case RequestUserHeaders:
 | 
					 | 
				
			||||||
			// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
 | 
					 | 
				
			||||||
			// Find and parse user headers serialized into a single one
 | 
					 | 
				
			||||||
			userHeaders, err := DeserializeHeaders(header.Value)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return errors.Wrap(err, "Unable to parse user headers")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			for _, userHeader := range userHeaders {
 | 
					 | 
				
			||||||
				h1.Header.Add(userHeader.Name, userHeader.Value)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		default:
 | 
					 | 
				
			||||||
			// All other control headers shall just be proxied transparently
 | 
					 | 
				
			||||||
			h1.Header.Add(header.Name, header.Value)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func IsControlRequestHeader(headerName string) bool {
 | 
					 | 
				
			||||||
	return headerName == "content-length" ||
 | 
					 | 
				
			||||||
		headerName == "connection" || headerName == "upgrade" || // Websocket request headers
 | 
					 | 
				
			||||||
		strings.HasPrefix(headerName, ":") ||
 | 
					 | 
				
			||||||
		strings.HasPrefix(headerName, "cf-")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func IsControlResponseHeader(headerName string) bool {
 | 
					func IsControlResponseHeader(headerName string) bool {
 | 
				
			||||||
	return headerName == "content-length" ||
 | 
						return strings.HasPrefix(headerName, ":") ||
 | 
				
			||||||
		strings.HasPrefix(headerName, ":") ||
 | 
					 | 
				
			||||||
		strings.HasPrefix(headerName, "cf-int-") ||
 | 
							strings.HasPrefix(headerName, "cf-int-") ||
 | 
				
			||||||
		strings.HasPrefix(headerName, "cf-cloudflared-")
 | 
							strings.HasPrefix(headerName, "cf-cloudflared-")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -142,31 +56,6 @@ func IsWebsocketClientHeader(headerName string) bool {
 | 
				
			||||||
		headerName == "upgrade"
 | 
							headerName == "upgrade"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
 | 
					 | 
				
			||||||
	h2 = []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: ":status", Value: strconv.Itoa(status)},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	userHeaders := make(http.Header, len(h1))
 | 
					 | 
				
			||||||
	for header, values := range h1 {
 | 
					 | 
				
			||||||
		h2name := strings.ToLower(header)
 | 
					 | 
				
			||||||
		if h2name == "content-length" {
 | 
					 | 
				
			||||||
			// This header has meaning in HTTP/2 and will be used by the edge,
 | 
					 | 
				
			||||||
			// so it should be sent as an HTTP/2 response header.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Since these are http2 headers, they're required to be lowercase
 | 
					 | 
				
			||||||
			h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
 | 
					 | 
				
			||||||
		} else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
 | 
					 | 
				
			||||||
			// User headers, on the other hand, must all be serialized so that
 | 
					 | 
				
			||||||
			// HTTP/2 header validation won't be applied to HTTP/1 header values
 | 
					 | 
				
			||||||
			userHeaders[header] = values
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Perform user header serialization and set them in the single header
 | 
					 | 
				
			||||||
	h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
 | 
					 | 
				
			||||||
	return h2
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Serialize HTTP1.x headers by base64-encoding each header name and value,
 | 
					// Serialize HTTP1.x headers by base64-encoding each header name and value,
 | 
				
			||||||
// and then joining them in the format of [key:value;]
 | 
					// and then joining them in the format of [key:value;]
 | 
				
			||||||
func SerializeHeaders(h1Headers http.Header) string {
 | 
					func SerializeHeaders(h1Headers http.Header) string {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,441 +2,14 @@ package connection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"math/rand"
 | 
					 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
					 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"regexp"
 | 
					 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"testing/quick"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
	"github.com/stretchr/testify/require"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/cloudflare/cloudflared/h2mux"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ByName []h2mux.Header
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (a ByName) Len() int      { return len(a) }
 | 
					 | 
				
			||||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
 | 
					 | 
				
			||||||
func (a ByName) Less(i, j int) bool {
 | 
					 | 
				
			||||||
	if a[i].Name == a[j].Name {
 | 
					 | 
				
			||||||
		return a[i].Value < a[j].Value
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return a[i].Name < a[j].Name
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
 | 
					 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mockHeaders := http.Header{
 | 
					 | 
				
			||||||
		"Mock header 1": {"Mock value 1"},
 | 
					 | 
				
			||||||
		"Mock header 2": {"Mock value 2"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
 | 
					 | 
				
			||||||
	assert.NoError(t, headersConversionErr)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
 | 
					 | 
				
			||||||
	return []h2mux.Header{{
 | 
					 | 
				
			||||||
		Name:  headersField,
 | 
					 | 
				
			||||||
		Value: SerializeHeaders(headers),
 | 
					 | 
				
			||||||
	}}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
 | 
					 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	emptyHeaders := make(http.Header)
 | 
					 | 
				
			||||||
	headersConversionErr := H2RequestHeadersToH1Request(
 | 
					 | 
				
			||||||
		[]h2mux.Header{{
 | 
					 | 
				
			||||||
			Name:  RequestUserHeaders,
 | 
					 | 
				
			||||||
			Value: SerializeHeaders(emptyHeaders),
 | 
					 | 
				
			||||||
		}},
 | 
					 | 
				
			||||||
		request,
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
 | 
					 | 
				
			||||||
	assert.NoError(t, headersConversionErr)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
 | 
					 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mockRequestHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: ":path", Value: "//bad_path/"},
 | 
					 | 
				
			||||||
		{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, http.Header{
 | 
					 | 
				
			||||||
		"Mock header": []string{"Mock value"},
 | 
					 | 
				
			||||||
	}, request.Header)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, headersConversionErr)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
 | 
					 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mockRequestHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: ":path", Value: "/?query=mock%20value"},
 | 
					 | 
				
			||||||
		{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, http.Header{
 | 
					 | 
				
			||||||
		"Mock header": []string{"Mock value"},
 | 
					 | 
				
			||||||
	}, request.Header)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, headersConversionErr)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
 | 
					 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mockRequestHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: ":path", Value: "/mock%20path"},
 | 
					 | 
				
			||||||
		{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, http.Header{
 | 
					 | 
				
			||||||
		"Mock header": []string{"Mock value"},
 | 
					 | 
				
			||||||
	}, request.Header)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.NoError(t, headersConversionErr)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
 | 
					 | 
				
			||||||
	type testCase struct {
 | 
					 | 
				
			||||||
		path string
 | 
					 | 
				
			||||||
		want string
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	testCases := []testCase{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "",
 | 
					 | 
				
			||||||
			want: "",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/",
 | 
					 | 
				
			||||||
			want: "/",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "//",
 | 
					 | 
				
			||||||
			want: "//",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/test",
 | 
					 | 
				
			||||||
			want: "/test",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "//test",
 | 
					 | 
				
			||||||
			want: "//test",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// https://github.com/cloudflare/cloudflared/issues/81
 | 
					 | 
				
			||||||
			path: "//test/",
 | 
					 | 
				
			||||||
			want: "//test/",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/%2Ftest",
 | 
					 | 
				
			||||||
			want: "/%2Ftest",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "//%20test",
 | 
					 | 
				
			||||||
			want: "//%20test",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// https://github.com/cloudflare/cloudflared/issues/124
 | 
					 | 
				
			||||||
			path: "/test?get=somthing%20a",
 | 
					 | 
				
			||||||
			want: "/test?get=somthing%20a",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/%20",
 | 
					 | 
				
			||||||
			want: "/%20",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// stdlib's EscapedPath() will always percent-encode ' '
 | 
					 | 
				
			||||||
			path: "/ ",
 | 
					 | 
				
			||||||
			want: "/%20",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/ a ",
 | 
					 | 
				
			||||||
			want: "/%20a%20",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a%20b",
 | 
					 | 
				
			||||||
			want: "/a%20b",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/foo/bar;param?query#frag",
 | 
					 | 
				
			||||||
			want: "/foo/bar;param?query#frag",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// stdlib's EscapedPath() will always percent-encode non-ASCII chars
 | 
					 | 
				
			||||||
			path: "/a␠b",
 | 
					 | 
				
			||||||
			want: "/a%E2%90%A0b",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a-umlaut-ä",
 | 
					 | 
				
			||||||
			want: "/a-umlaut-%C3%A4",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a-umlaut-%C3%A4",
 | 
					 | 
				
			||||||
			want: "/a-umlaut-%C3%A4",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a-umlaut-%c3%a4",
 | 
					 | 
				
			||||||
			want: "/a-umlaut-%c3%a4",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// here the second '#' is treated as part of the fragment
 | 
					 | 
				
			||||||
			path: "/a#b#c",
 | 
					 | 
				
			||||||
			want: "/a#b%23c",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a#b␠c",
 | 
					 | 
				
			||||||
			want: "/a#b%E2%90%A0c",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a#b%20c",
 | 
					 | 
				
			||||||
			want: "/a#b%20c",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a#b c",
 | 
					 | 
				
			||||||
			want: "/a#b%20c",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// stdlib's EscapedPath() will always percent-encode '\'
 | 
					 | 
				
			||||||
			path: "/\\",
 | 
					 | 
				
			||||||
			want: "/%5C",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a\\",
 | 
					 | 
				
			||||||
			want: "/a%5C",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a,b.c.",
 | 
					 | 
				
			||||||
			want: "/a,b.c.",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/.",
 | 
					 | 
				
			||||||
			want: "/.",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// stdlib's EscapedPath() will always percent-encode '`'
 | 
					 | 
				
			||||||
			path: "/a`",
 | 
					 | 
				
			||||||
			want: "/a%60",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/a[0]",
 | 
					 | 
				
			||||||
			want: "/a[0]",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/?a[0]=5 &b[]=",
 | 
					 | 
				
			||||||
			want: "/?a[0]=5 &b[]=",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			path: "/?a=%22b%20%22",
 | 
					 | 
				
			||||||
			want: "/?a=%22b%20%22",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for index, testCase := range testCases {
 | 
					 | 
				
			||||||
		requestURL := "https://example.com"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		request, err := http.NewRequest(http.MethodGet, requestURL, nil)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		mockRequestHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
			{Name: ":path", Value: testCase.path},
 | 
					 | 
				
			||||||
			{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
 | 
					 | 
				
			||||||
		assert.NoError(t, headersConversionErr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		assert.Equal(t,
 | 
					 | 
				
			||||||
			http.Header{
 | 
					 | 
				
			||||||
				"Mock header": []string{"Mock value"},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			request.Header)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		assert.Equal(t,
 | 
					 | 
				
			||||||
			"https://example.com"+testCase.want,
 | 
					 | 
				
			||||||
			request.URL.String(),
 | 
					 | 
				
			||||||
			"Failed URL index: %v %#v", index, testCase)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
 | 
					 | 
				
			||||||
	config := &quick.Config{
 | 
					 | 
				
			||||||
		Values: func(args []reflect.Value, rand *rand.Rand) {
 | 
					 | 
				
			||||||
			args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	type testOrigin struct {
 | 
					 | 
				
			||||||
		url string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		expectedScheme   string
 | 
					 | 
				
			||||||
		expectedBasePath string
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	testOrigins := []testOrigin{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			url:              "http://origin.hostname.example.com:8080",
 | 
					 | 
				
			||||||
			expectedScheme:   "http",
 | 
					 | 
				
			||||||
			expectedBasePath: "http://origin.hostname.example.com:8080",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			url:              "http://origin.hostname.example.com:8080/",
 | 
					 | 
				
			||||||
			expectedScheme:   "http",
 | 
					 | 
				
			||||||
			expectedBasePath: "http://origin.hostname.example.com:8080",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			url:              "http://origin.hostname.example.com:8080/api",
 | 
					 | 
				
			||||||
			expectedScheme:   "http",
 | 
					 | 
				
			||||||
			expectedBasePath: "http://origin.hostname.example.com:8080/api",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			url:              "http://origin.hostname.example.com:8080/api/",
 | 
					 | 
				
			||||||
			expectedScheme:   "http",
 | 
					 | 
				
			||||||
			expectedBasePath: "http://origin.hostname.example.com:8080/api",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			url:              "https://origin.hostname.example.com:8080/api",
 | 
					 | 
				
			||||||
			expectedScheme:   "https",
 | 
					 | 
				
			||||||
			expectedBasePath: "https://origin.hostname.example.com:8080/api",
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// use multiple schemes to demonstrate that the URL is based on the
 | 
					 | 
				
			||||||
	// origin's scheme, not the :scheme header
 | 
					 | 
				
			||||||
	for _, testScheme := range []string{"http", "https"} {
 | 
					 | 
				
			||||||
		for _, testOrigin := range testOrigins {
 | 
					 | 
				
			||||||
			assertion := func(testPath string) bool {
 | 
					 | 
				
			||||||
				const expectedMethod = "POST"
 | 
					 | 
				
			||||||
				const expectedHostname = "request.hostname.example.com"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				h2 := []h2mux.Header{
 | 
					 | 
				
			||||||
					{Name: ":method", Value: expectedMethod},
 | 
					 | 
				
			||||||
					{Name: ":scheme", Value: testScheme},
 | 
					 | 
				
			||||||
					{Name: ":authority", Value: expectedHostname},
 | 
					 | 
				
			||||||
					{Name: ":path", Value: testPath},
 | 
					 | 
				
			||||||
					{Name: RequestUserHeaders, Value: ""},
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				h1, err := http.NewRequest("GET", testOrigin.url, nil)
 | 
					 | 
				
			||||||
				require.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				err = H2RequestHeadersToH1Request(h2, h1)
 | 
					 | 
				
			||||||
				return assert.NoError(t, err) &&
 | 
					 | 
				
			||||||
					assert.Equal(t, expectedMethod, h1.Method) &&
 | 
					 | 
				
			||||||
					assert.Equal(t, expectedHostname, h1.Host) &&
 | 
					 | 
				
			||||||
					assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
 | 
					 | 
				
			||||||
					assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			err := quick.Check(assertion, config)
 | 
					 | 
				
			||||||
			assert.NoError(t, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func randomASCIIPrintableChar(rand *rand.Rand) int {
 | 
					 | 
				
			||||||
	// smallest printable ASCII char is 32, largest is 126
 | 
					 | 
				
			||||||
	const startPrintable = 32
 | 
					 | 
				
			||||||
	const endPrintable = 127
 | 
					 | 
				
			||||||
	return startPrintable + rand.Intn(endPrintable-startPrintable)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// randomASCIIText generates an ASCII string, some of whose characters may be
 | 
					 | 
				
			||||||
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
 | 
					 | 
				
			||||||
// between 1 and `maxLength`.
 | 
					 | 
				
			||||||
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
 | 
					 | 
				
			||||||
	length := minLength + rand.Intn(maxLength)
 | 
					 | 
				
			||||||
	var result strings.Builder
 | 
					 | 
				
			||||||
	for i := 0; i < length; i++ {
 | 
					 | 
				
			||||||
		c := randomASCIIPrintableChar(rand)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 1/4 chance of using percent encoding when not necessary
 | 
					 | 
				
			||||||
		if c == '%' || rand.Intn(4) == 0 {
 | 
					 | 
				
			||||||
			result.WriteString(fmt.Sprintf("%%%02X", c))
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			result.WriteByte(byte(c))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return result.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Calls `randomASCIIText` and ensures the result is a valid URL path,
 | 
					 | 
				
			||||||
// i.e. one that can pass unchanged through url.URL.String()
 | 
					 | 
				
			||||||
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
 | 
					 | 
				
			||||||
	text := randomASCIIText(rand, minLength, maxLength)
 | 
					 | 
				
			||||||
	re, err := regexp.Compile("[^/;,]*")
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
	return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Calls `randomASCIIText` and ensures the result is a valid URL query,
 | 
					 | 
				
			||||||
// i.e. one that can pass unchanged through url.URL.String()
 | 
					 | 
				
			||||||
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
 | 
					 | 
				
			||||||
	text := randomASCIIText(rand, minLength, maxLength)
 | 
					 | 
				
			||||||
	return "?" + strings.ReplaceAll(text, "#", "%23")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
 | 
					 | 
				
			||||||
// i.e. one that can pass unchanged through url.URL.String()
 | 
					 | 
				
			||||||
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
 | 
					 | 
				
			||||||
	text := randomASCIIText(rand, minLength, maxLength)
 | 
					 | 
				
			||||||
	u, err := url.Parse("#" + text)
 | 
					 | 
				
			||||||
	require.NoError(t, err)
 | 
					 | 
				
			||||||
	return u.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
 | 
					 | 
				
			||||||
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
 | 
					 | 
				
			||||||
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
 | 
					 | 
				
			||||||
	result := randomHTTP1Path(t, rand, 1, 64)
 | 
					 | 
				
			||||||
	if rand.Intn(2) == 1 {
 | 
					 | 
				
			||||||
		result += randomHTTP1Query(rand, 1, 32)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if rand.Intn(2) == 1 {
 | 
					 | 
				
			||||||
		result += randomHTTP1Fragment(t, rand, 1, 16)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return result
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
 | 
					 | 
				
			||||||
	for name, values := range headers {
 | 
					 | 
				
			||||||
		for _, value := range values {
 | 
					 | 
				
			||||||
			h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return h2muxHeaders
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestSerializeHeaders(t *testing.T) {
 | 
					func TestSerializeHeaders(t *testing.T) {
 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
						request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
| 
						 | 
					@ -511,70 +84,13 @@ func TestDeserializeMalformed(t *testing.T) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestParseRequestHeaders(t *testing.T) {
 | 
					 | 
				
			||||||
	mockUserHeadersToSerialize := http.Header{
 | 
					 | 
				
			||||||
		"Mock-Header-One":   {"1", "1.5"},
 | 
					 | 
				
			||||||
		"Mock-Header-Two":   {"2"},
 | 
					 | 
				
			||||||
		"Mock-Header-Three": {"3"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mockHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: "One", Value: "1"}, // will be dropped
 | 
					 | 
				
			||||||
		{Name: "Cf-Two", Value: "cf-value-1"},
 | 
					 | 
				
			||||||
		{Name: "Cf-Two", Value: "cf-value-2"},
 | 
					 | 
				
			||||||
		{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	expectedHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: "Cf-Two", Value: "cf-value-1"},
 | 
					 | 
				
			||||||
		{Name: "Cf-Two", Value: "cf-value-2"},
 | 
					 | 
				
			||||||
		{Name: "Mock-Header-One", Value: "1"},
 | 
					 | 
				
			||||||
		{Name: "Mock-Header-One", Value: "1.5"},
 | 
					 | 
				
			||||||
		{Name: "Mock-Header-Two", Value: "2"},
 | 
					 | 
				
			||||||
		{Name: "Mock-Header-Three", Value: "3"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	h1 := &http.Request{
 | 
					 | 
				
			||||||
		Header: make(http.Header),
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	err := H2RequestHeadersToH1Request(mockHeaders, h1)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsControlRequestHeader(t *testing.T) {
 | 
					 | 
				
			||||||
	controlRequestHeaders := []string{
 | 
					 | 
				
			||||||
		// Anything that begins with cf-
 | 
					 | 
				
			||||||
		"cf-sample-header",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Any http2 pseudoheader
 | 
					 | 
				
			||||||
		":sample-pseudo-header",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// content-length is a special case, it has to be there
 | 
					 | 
				
			||||||
		// for some requests to work (per the HTTP2 spec)
 | 
					 | 
				
			||||||
		"content-length",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Websocket request headers
 | 
					 | 
				
			||||||
		"connection",
 | 
					 | 
				
			||||||
		"upgrade",
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, header := range controlRequestHeaders {
 | 
					 | 
				
			||||||
		assert.True(t, IsControlRequestHeader(header))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsControlResponseHeader(t *testing.T) {
 | 
					func TestIsControlResponseHeader(t *testing.T) {
 | 
				
			||||||
	controlResponseHeaders := []string{
 | 
						controlResponseHeaders := []string{
 | 
				
			||||||
		// Anything that begins with cf-int- or cf-cloudflared-
 | 
							// Anything that begins with cf-int- or cf-cloudflared-
 | 
				
			||||||
		"cf-int-sample-header",
 | 
							"cf-int-sample-header",
 | 
				
			||||||
		"cf-cloudflared-sample-header",
 | 
							"cf-cloudflared-sample-header",
 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Any http2 pseudoheader
 | 
							// Any http2 pseudoheader
 | 
				
			||||||
		":sample-pseudo-header",
 | 
							":sample-pseudo-header",
 | 
				
			||||||
 | 
					 | 
				
			||||||
		// content-length is a special case, it has to be there
 | 
					 | 
				
			||||||
		// for some requests to work (per the HTTP2 spec)
 | 
					 | 
				
			||||||
		"content-length",
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, header := range controlResponseHeaders {
 | 
						for _, header := range controlResponseHeaders {
 | 
				
			||||||
| 
						 | 
					@ -582,17 +98,6 @@ func TestIsControlResponseHeader(t *testing.T) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestIsNotControlRequestHeader(t *testing.T) {
 | 
					 | 
				
			||||||
	notControlRequestHeaders := []string{
 | 
					 | 
				
			||||||
		"mock-header",
 | 
					 | 
				
			||||||
		"another-sample-header",
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, header := range notControlRequestHeaders {
 | 
					 | 
				
			||||||
		assert.False(t, IsControlRequestHeader(header))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestIsNotControlResponseHeader(t *testing.T) {
 | 
					func TestIsNotControlResponseHeader(t *testing.T) {
 | 
				
			||||||
	notControlResponseHeaders := []string{
 | 
						notControlResponseHeaders := []string{
 | 
				
			||||||
		"mock-header",
 | 
							"mock-header",
 | 
				
			||||||
| 
						 | 
					@ -606,112 +111,3 @@ func TestIsNotControlResponseHeader(t *testing.T) {
 | 
				
			||||||
		assert.False(t, IsControlResponseHeader(header))
 | 
							assert.False(t, IsControlResponseHeader(header))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
 | 
					 | 
				
			||||||
	mockHeaders := http.Header{
 | 
					 | 
				
			||||||
		"User-header-one":       {""},
 | 
					 | 
				
			||||||
		"User-header-two":       {"1", "2"},
 | 
					 | 
				
			||||||
		"cf-header":             {"cf-value"},
 | 
					 | 
				
			||||||
		"cf-int-header":         {"cf-int-value"},
 | 
					 | 
				
			||||||
		"cf-cloudflared-header": {"cf-cloudflared-value"},
 | 
					 | 
				
			||||||
		"Content-Length":        {"123"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	mockResponse := http.Response{
 | 
					 | 
				
			||||||
		StatusCode: 200,
 | 
					 | 
				
			||||||
		Header:     mockHeaders,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	serializedHeadersIndex := -1
 | 
					 | 
				
			||||||
	for i, header := range headers {
 | 
					 | 
				
			||||||
		if header.Name == ResponseUserHeaders {
 | 
					 | 
				
			||||||
			serializedHeadersIndex = i
 | 
					 | 
				
			||||||
			break
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	assert.NotEqual(t, -1, serializedHeadersIndex)
 | 
					 | 
				
			||||||
	actualControlHeaders := append(
 | 
					 | 
				
			||||||
		headers[:serializedHeadersIndex],
 | 
					 | 
				
			||||||
		headers[serializedHeadersIndex+1:]...,
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	expectedControlHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: ":status", Value: "200"},
 | 
					 | 
				
			||||||
		{Name: "content-length", Value: "123"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
 | 
					 | 
				
			||||||
	expectedUserHeaders := []h2mux.Header{
 | 
					 | 
				
			||||||
		{Name: "User-header-one", Value: ""},
 | 
					 | 
				
			||||||
		{Name: "User-header-two", Value: "1"},
 | 
					 | 
				
			||||||
		{Name: "User-header-two", Value: "2"},
 | 
					 | 
				
			||||||
		{Name: "cf-header", Value: "cf-value"},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
	assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// The purpose of this test is to check that our code and the http.Header
 | 
					 | 
				
			||||||
// implementation don't throw validation errors about header size
 | 
					 | 
				
			||||||
func TestHeaderSize(t *testing.T) {
 | 
					 | 
				
			||||||
	largeValue := randSeq(5 * 1024 * 1024) // 5Mb
 | 
					 | 
				
			||||||
	largeHeaders := http.Header{
 | 
					 | 
				
			||||||
		"User-header": {largeValue},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	mockResponse := http.Response{
 | 
					 | 
				
			||||||
		StatusCode: 200,
 | 
					 | 
				
			||||||
		Header:     largeHeaders,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
 | 
					 | 
				
			||||||
	request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
 | 
					 | 
				
			||||||
	assert.NoError(t, err)
 | 
					 | 
				
			||||||
	for _, header := range serializedHeaders {
 | 
					 | 
				
			||||||
		request.Header.Set(header.Name, header.Value)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, header := range serializedHeaders {
 | 
					 | 
				
			||||||
		if header.Name != ResponseUserHeaders {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		deserializedHeaders, err := DeserializeHeaders(header.Value)
 | 
					 | 
				
			||||||
		assert.NoError(t, err)
 | 
					 | 
				
			||||||
		assert.Equal(t, largeValue, deserializedHeaders[0].Value)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func randSeq(n int) string {
 | 
					 | 
				
			||||||
	randomizer := rand.New(rand.NewSource(17))
 | 
					 | 
				
			||||||
	var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
 | 
					 | 
				
			||||||
	b := make([]rune, n)
 | 
					 | 
				
			||||||
	for i := range b {
 | 
					 | 
				
			||||||
		b[i] = letters[randomizer.Intn(len(letters))]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return string(b)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
 | 
					 | 
				
			||||||
	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"
 | 
					 | 
				
			||||||
	h2, _ := DeserializeHeaders(ser)
 | 
					 | 
				
			||||||
	h1 := make(http.Header)
 | 
					 | 
				
			||||||
	for _, header := range h2 {
 | 
					 | 
				
			||||||
		h1.Add(header.Name, header.Value)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	h1.Add("Content-Length", "200")
 | 
					 | 
				
			||||||
	h1.Add("Cf-Something", "Else")
 | 
					 | 
				
			||||||
	h1.Add("Upgrade", "websocket")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	h1resp := &http.Response{
 | 
					 | 
				
			||||||
		StatusCode: 200,
 | 
					 | 
				
			||||||
		Header:     h1,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	b.ReportAllocs()
 | 
					 | 
				
			||||||
	b.ResetTimer()
 | 
					 | 
				
			||||||
	for i := 0; i < b.N; i++ {
 | 
					 | 
				
			||||||
		_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -184,12 +184,14 @@ func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) erro
 | 
				
			||||||
	for name, values := range header {
 | 
						for name, values := range header {
 | 
				
			||||||
		// Since these are http2 headers, they're required to be lowercase
 | 
							// Since these are http2 headers, they're required to be lowercase
 | 
				
			||||||
		h2name := strings.ToLower(name)
 | 
							h2name := strings.ToLower(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if h2name == "content-length" {
 | 
							if h2name == "content-length" {
 | 
				
			||||||
			// This header has meaning in HTTP/2 and will be used by the edge,
 | 
								// This header has meaning in HTTP/2 and will be used by the edge,
 | 
				
			||||||
			// so it should be sent as an HTTP/2 response header.
 | 
								// so it should be sent *also* as an HTTP/2 response header.
 | 
				
			||||||
			dest[name] = values
 | 
								dest[name] = values
 | 
				
			||||||
			// Since these are http2 headers, they're required to be lowercase
 | 
							}
 | 
				
			||||||
		} else if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
 | 
					
 | 
				
			||||||
 | 
							if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
 | 
				
			||||||
			// User headers, on the other hand, must all be serialized so that
 | 
								// User headers, on the other hand, must all be serialized so that
 | 
				
			||||||
			// HTTP/2 header validation won't be applied to HTTP/1 header values
 | 
								// HTTP/2 header validation won't be applied to HTTP/1 header values
 | 
				
			||||||
			userHeaders[name] = values
 | 
								userHeaders[name] = values
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue