TUN-5029: Do not strip cf- prefixed headers

This commit is contained in:
Areg Harutyunyan 2021-08-31 11:33:10 -05:00
parent 89d408e3bd
commit d04f48d872
No known key found for this signature in database
GPG Key ID: 97A3DFFE8E9320B7
3 changed files with 65 additions and 18 deletions

View File

@ -54,7 +54,7 @@ const ()
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error { func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
for _, header := range h2 { for _, header := range h2 {
name := strings.ToLower(header.Name) name := strings.ToLower(header.Name)
if !IsControlHeader(name) { if !IsControlRequestHeader(name) {
continue continue
} }
@ -121,13 +121,20 @@ func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
return nil return nil
} }
func IsControlHeader(headerName string) bool { func IsControlRequestHeader(headerName string) bool {
return headerName == "content-length" || return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket headers headerName == "connection" || headerName == "upgrade" || // Websocket request headers
strings.HasPrefix(headerName, ":") || strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-") 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 // isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func IsWebsocketClientHeader(headerName string) bool { func IsWebsocketClientHeader(headerName string) bool {
return headerName == "sec-websocket-accept" || 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 // Since these are http2 headers, they're required to be lowercase
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]}) 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 // 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[header] = values userHeaders[header] = values

View File

@ -511,7 +511,7 @@ func TestDeserializeMalformed(t *testing.T) {
} }
} }
func TestParseHeaders(t *testing.T) { func TestParseRequestHeaders(t *testing.T) {
mockUserHeadersToSerialize := http.Header{ mockUserHeadersToSerialize := http.Header{
"Mock-Header-One": {"1", "1.5"}, "Mock-Header-One": {"1", "1.5"},
"Mock-Header-Two": {"2"}, "Mock-Header-Two": {"2"},
@ -541,8 +541,8 @@ func TestParseHeaders(t *testing.T) {
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header)) assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
} }
func TestIsControlHeader(t *testing.T) { func TestIsControlRequestHeader(t *testing.T) {
controlHeaders := []string{ controlRequestHeaders := []string{
// Anything that begins with cf- // Anything that begins with cf-
"cf-sample-header", "cf-sample-header",
@ -552,30 +552,69 @@ func TestIsControlHeader(t *testing.T) {
// content-length is a special case, it has to be there // content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec) // for some requests to work (per the HTTP2 spec)
"content-length", "content-length",
// Websocket request headers
"connection",
"upgrade",
} }
for _, header := range controlHeaders { for _, header := range controlRequestHeaders {
assert.True(t, IsControlHeader(header)) assert.True(t, IsControlRequestHeader(header))
} }
} }
func TestIsNotControlHeader(t *testing.T) { func TestIsControlResponseHeader(t *testing.T) {
notControlHeaders := []string{ 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", "mock-header",
"another-sample-header", "another-sample-header",
} }
for _, header := range notControlHeaders { for _, header := range notControlRequestHeaders {
assert.False(t, IsControlHeader(header)) 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) { func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
mockHeaders := http.Header{ mockHeaders := http.Header{
"User-header-one": {""}, "User-header-one": {""},
"User-header-two": {"1", "2"}, "User-header-two": {"1", "2"},
"cf-header": {"cf-value"}, "cf-header": {"cf-value"},
"Content-Length": {"123"}, "cf-int-header": {"cf-int-value"},
"cf-cloudflared-header": {"cf-cloudflared-value"},
"Content-Length": {"123"},
} }
mockResponse := http.Response{ mockResponse := http.Response{
StatusCode: 200, StatusCode: 200,
@ -608,6 +647,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
{Name: "User-header-one", Value: ""}, {Name: "User-header-one", Value: ""},
{Name: "User-header-two", Value: "1"}, {Name: "User-header-two", Value: "1"},
{Name: "User-header-two", Value: "2"}, {Name: "User-header-two", Value: "2"},
{Name: "cf-header", Value: "cf-value"},
} }
assert.NoError(t, err) assert.NoError(t, err)
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders) assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)

View File

@ -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. // so it should be sent as an HTTP/2 response header.
dest[name] = values dest[name] = values
// Since these are http2 headers, they're required to be lowercase // 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 // 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