From d04f48d8727e375bc5ea1bc722a598bd8afb4c99 Mon Sep 17 00:00:00 2001 From: Areg Harutyunyan Date: Tue, 31 Aug 2021 11:33:10 -0500 Subject: [PATCH] TUN-5029: Do not strip cf- prefixed headers --- connection/header.go | 15 ++++++--- connection/header_test.go | 66 +++++++++++++++++++++++++++++++-------- connection/http2.go | 2 +- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/connection/header.go b/connection/header.go index c93c1073..61242f6b 100644 --- a/connection/header.go +++ b/connection/header.go @@ -54,7 +54,7 @@ const () func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error { for _, header := range h2 { name := strings.ToLower(header.Name) - if !IsControlHeader(name) { + if !IsControlRequestHeader(name) { continue } @@ -121,13 +121,20 @@ func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error { return nil } -func IsControlHeader(headerName string) bool { +func IsControlRequestHeader(headerName string) bool { return headerName == "content-length" || - headerName == "connection" || headerName == "upgrade" || // Websocket headers + headerName == "connection" || headerName == "upgrade" || // Websocket request headers strings.HasPrefix(headerName, ":") || strings.HasPrefix(headerName, "cf-") } +func IsControlResponseHeader(headerName string) bool { + return headerName == "content-length" || + strings.HasPrefix(headerName, ":") || + strings.HasPrefix(headerName, "cf-int-") || + strings.HasPrefix(headerName, "cf-cloudflared-") +} + // isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly func IsWebsocketClientHeader(headerName string) bool { return headerName == "sec-websocket-accept" || @@ -148,7 +155,7 @@ func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Heade // Since these are http2 headers, they're required to be lowercase h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]}) - } else if !IsControlHeader(h2name) || IsWebsocketClientHeader(h2name) { + } 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 diff --git a/connection/header_test.go b/connection/header_test.go index 964e7751..ad6881b1 100644 --- a/connection/header_test.go +++ b/connection/header_test.go @@ -511,7 +511,7 @@ func TestDeserializeMalformed(t *testing.T) { } } -func TestParseHeaders(t *testing.T) { +func TestParseRequestHeaders(t *testing.T) { mockUserHeadersToSerialize := http.Header{ "Mock-Header-One": {"1", "1.5"}, "Mock-Header-Two": {"2"}, @@ -541,8 +541,8 @@ func TestParseHeaders(t *testing.T) { assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header)) } -func TestIsControlHeader(t *testing.T) { - controlHeaders := []string{ +func TestIsControlRequestHeader(t *testing.T) { + controlRequestHeaders := []string{ // Anything that begins with cf- "cf-sample-header", @@ -552,30 +552,69 @@ func TestIsControlHeader(t *testing.T) { // 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 controlHeaders { - assert.True(t, IsControlHeader(header)) + for _, header := range controlRequestHeaders { + assert.True(t, IsControlRequestHeader(header)) } } -func TestIsNotControlHeader(t *testing.T) { - notControlHeaders := []string{ +func TestIsControlResponseHeader(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, IsControlResponseHeader(header)) + } +} + +func TestIsNotControlRequestHeader(t *testing.T) { + notControlRequestHeaders := []string{ "mock-header", "another-sample-header", } - for _, header := range notControlHeaders { - assert.False(t, IsControlHeader(header)) + for _, header := range notControlRequestHeaders { + assert.False(t, IsControlRequestHeader(header)) + } +} + +func TestIsNotControlResponseHeader(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, IsControlResponseHeader(header)) } } func TestH1ResponseToH2ResponseHeaders(t *testing.T) { mockHeaders := http.Header{ - "User-header-one": {""}, - "User-header-two": {"1", "2"}, - "cf-header": {"cf-value"}, - "Content-Length": {"123"}, + "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, @@ -608,6 +647,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) { {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) diff --git a/connection/http2.go b/connection/http2.go index bd3c2648..c2ed6b17 100644 --- a/connection/http2.go +++ b/connection/http2.go @@ -189,7 +189,7 @@ func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) erro // so it should be sent as an HTTP/2 response header. dest[name] = values // Since these are http2 headers, they're required to be lowercase - } else if !IsControlHeader(h2name) || IsWebsocketClientHeader(h2name) { + } 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[name] = values