Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/content/cli/forwarder_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`
Expand Down
45 changes: 34 additions & 11 deletions header/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
RemoveByPrefix
Empty
Add
RenameCase
)

type Header struct {
Expand All @@ -35,32 +36,34 @@ var (

// ParseHeader supports the following syntax:
// - "<name>: <value>" to add a header,
// - "%<name>" to replace canonical header name to custom case
// - "<name>;" to set a header to empty,
// - "-<name>" to remove a header,
// - "-<name>*" 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
} else {
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")
}
}

Expand All @@ -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)
}
}
}

Expand All @@ -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 ""
}
Expand Down
27 changes: 27 additions & 0 deletions header/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

func TestParseHeader(t *testing.T) {
tests := []struct {
input string
expected Header
}{
{
input: "%rename-me",
expected: Header{
Name: "rename-me",
Action: RenameCase,
},
},
{
input: "-RemoveMe",
expected: Header{
Expand Down Expand Up @@ -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)
}
Loading