diff --git a/docs/content/cli/forwarder_run.md b/docs/content/cli/forwarder_run.md index ac1eaba2..eb3f386d 100644 --- a/docs/content/cli/forwarder_run.md +++ b/docs/content/cli/forwarder_run.md @@ -217,6 +217,7 @@ Use the format: - name; to set the header to empty value - -name to remove the header - -name* to remove headers by prefix +- %name to disable header name canonicalization for particular header name The header name will be normalized to canonical form. The header value should not contain any newlines or carriage returns. @@ -227,6 +228,25 @@ The following example removes the User-Agent header and all headers starting wit -H "-User-Agent" -H "-X-*" ``` +#### Disabling header canonicalization + +By default all headers received from a request are being canonicalized and this can not be disabled. In some rare cases +destination HTTP servers or load balancers break HTTP standards +and treat header names as case sensitive. + +So if browser sends `header-a`, forwarder canonicalizes the name to `Header-A` and the target application breaks, because it expects different name. The "%" option allows to change particular header case if needed. + +For example + +``` + -H "%header-a" +``` + +If any case form of provided header name exists in request it will be renamed to the exact form provided. In that case browser can send +`header-a`, forwarder will canonicalize it +to `Header-A` but this option will rename it back to `header-a`. + + ### `-p, --pac` {#pac} * Environment variable: `FORWARDER_PAC` diff --git a/header/header.go b/header/header.go index dd825dbd..e9c32f87 100644 --- a/header/header.go +++ b/header/header.go @@ -20,6 +20,7 @@ const ( RemoveByPrefix Empty Add + RenameCase ) type Header struct { @@ -35,13 +36,14 @@ var ( // ParseHeader supports the following syntax: // - ": " to add a header, +// - "%" to replace canonical header name to custom case // - ";" to set a header to empty, // - "-" to remove a header, // - "-*" to remove a header by prefix. func ParseHeader(val string) (Header, error) { var h Header - if strings.HasPrefix(val, "-") { + if strings.HasPrefix(val, "-") { //nolint if strings.HasSuffix(val, "*") { h.Name = val[1 : len(val)-1] h.Action = RemoveByPrefix @@ -49,18 +51,19 @@ func ParseHeader(val string) (Header, error) { h.Name = val[1:] h.Action = Remove } + } else if strings.HasPrefix(val, "%") { + h.Name = val[1:] + h.Action = RenameCase + } else if strings.HasSuffix(val, ";") { + h.Name = val[0 : len(val)-1] + h.Action = Empty } else { - if strings.HasSuffix(val, ";") { - h.Name = val[0 : len(val)-1] - h.Action = Empty + if m := headerLineRegex.FindStringSubmatch(val); m != nil { + h.Name = m[1] + h.Value = &m[2] + h.Action = Add } else { - if m := headerLineRegex.FindStringSubmatch(val); m != nil { - h.Name = m[1] - h.Value = &m[2] - h.Action = Add - } else { - return Header{}, errors.New("invalid header value") - } + return Header{}, errors.New("invalid header value") } } @@ -81,6 +84,24 @@ func (h *Header) Apply(hh http.Header) { hh.Set(h.Name, "") case Add: hh.Add(h.Name, *h.Value) + case RenameCase: + // RenameCase action is a workaround for some braindead HTTP software stacks + // which treat header names as case sensitive and which break when receiving + // HTTP(S) requests with headers having canonicalized names + // eg: browser sends "timestamp" header, forwarder changes it to "Timestamp" + // and server crashes. + + // To achieve this funcionality we utilize http.Header type being a map + // and replace canonicalized key with raw name + + canonicalizedName := http.CanonicalHeaderKey(h.Name) + + _, ok := hh[canonicalizedName] + + if ok { // key exists, replace it + hh[h.Name] = hh[canonicalizedName] + delete(hh, canonicalizedName) + } } } @@ -105,6 +126,8 @@ func (h *Header) String() string { return h.Name + ";" case Add: return h.Name + ":" + *h.Value + case RenameCase: + return "%" + h.Name default: return "" } diff --git a/header/header_test.go b/header/header_test.go index ac1aab4d..63fbe9f0 100644 --- a/header/header_test.go +++ b/header/header_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" ) func TestParseHeader(t *testing.T) { @@ -18,6 +19,13 @@ func TestParseHeader(t *testing.T) { input string expected Header }{ + { + input: "%rename-me", + expected: Header{ + Name: "rename-me", + Action: RenameCase, + }, + }, { input: "-RemoveMe", expected: Header{ @@ -160,3 +168,22 @@ func TestRemoveHeadersByPrefix(t *testing.T) { }) } } + +func TestReplaceCaseHeaderApply(t *testing.T) { + // force header to be in non-canonicalised form according to pattern + header := Header{ + Name: "rename-mE", + Action: RenameCase, + } + + httpHeader := make(http.Header) + + httpHeader.Add("Rename-Me", "true") + header.Apply(httpHeader) + + _, ok := httpHeader["rename-mE"] //nolint + require.True(t, ok) + + _, ok = httpHeader["Rename-Me"] + require.False(t, ok) +}