Skip to content

Commit d023f91

Browse files
support renaming heades to non-canonicalized case (#1111)
* support renaming heades to non-canonicalized case * update documentation
1 parent 97517e9 commit d023f91

3 files changed

Lines changed: 81 additions & 11 deletions

File tree

docs/content/cli/forwarder_run.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ Use the format:
217217
- name; to set the header to empty value
218218
- -name to remove the header
219219
- -name* to remove headers by prefix
220+
- %name to disable header name canonicalization for particular header name
220221

221222
The header name will be normalized to canonical form.
222223
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
227228
-H "-User-Agent" -H "-X-*"
228229
```
229230

231+
#### Disabling header canonicalization
232+
233+
By default all headers received from a request are being canonicalized and this can not be disabled. In some rare cases
234+
destination HTTP servers or load balancers break HTTP standards
235+
and treat header names as case sensitive.
236+
237+
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.
238+
239+
For example
240+
241+
```
242+
-H "%header-a"
243+
```
244+
245+
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
246+
`header-a`, forwarder will canonicalize it
247+
to `Header-A` but this option will rename it back to `header-a`.
248+
249+
230250
### `-p, --pac` {#pac}
231251

232252
* Environment variable: `FORWARDER_PAC`

header/header.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
RemoveByPrefix
2121
Empty
2222
Add
23+
RenameCase
2324
)
2425

2526
type Header struct {
@@ -35,32 +36,34 @@ var (
3536

3637
// ParseHeader supports the following syntax:
3738
// - "<name>: <value>" to add a header,
39+
// - "%<name>" to replace canonical header name to custom case
3840
// - "<name>;" to set a header to empty,
3941
// - "-<name>" to remove a header,
4042
// - "-<name>*" to remove a header by prefix.
4143
func ParseHeader(val string) (Header, error) {
4244
var h Header
4345

44-
if strings.HasPrefix(val, "-") {
46+
if strings.HasPrefix(val, "-") { //nolint
4547
if strings.HasSuffix(val, "*") {
4648
h.Name = val[1 : len(val)-1]
4749
h.Action = RemoveByPrefix
4850
} else {
4951
h.Name = val[1:]
5052
h.Action = Remove
5153
}
54+
} else if strings.HasPrefix(val, "%") {
55+
h.Name = val[1:]
56+
h.Action = RenameCase
57+
} else if strings.HasSuffix(val, ";") {
58+
h.Name = val[0 : len(val)-1]
59+
h.Action = Empty
5260
} else {
53-
if strings.HasSuffix(val, ";") {
54-
h.Name = val[0 : len(val)-1]
55-
h.Action = Empty
61+
if m := headerLineRegex.FindStringSubmatch(val); m != nil {
62+
h.Name = m[1]
63+
h.Value = &m[2]
64+
h.Action = Add
5665
} else {
57-
if m := headerLineRegex.FindStringSubmatch(val); m != nil {
58-
h.Name = m[1]
59-
h.Value = &m[2]
60-
h.Action = Add
61-
} else {
62-
return Header{}, errors.New("invalid header value")
63-
}
66+
return Header{}, errors.New("invalid header value")
6467
}
6568
}
6669

@@ -81,6 +84,24 @@ func (h *Header) Apply(hh http.Header) {
8184
hh.Set(h.Name, "")
8285
case Add:
8386
hh.Add(h.Name, *h.Value)
87+
case RenameCase:
88+
// RenameCase action is a workaround for some braindead HTTP software stacks
89+
// which treat header names as case sensitive and which break when receiving
90+
// HTTP(S) requests with headers having canonicalized names
91+
// eg: browser sends "timestamp" header, forwarder changes it to "Timestamp"
92+
// and server crashes.
93+
94+
// To achieve this funcionality we utilize http.Header type being a map
95+
// and replace canonicalized key with raw name
96+
97+
canonicalizedName := http.CanonicalHeaderKey(h.Name)
98+
99+
_, ok := hh[canonicalizedName]
100+
101+
if ok { // key exists, replace it
102+
hh[h.Name] = hh[canonicalizedName]
103+
delete(hh, canonicalizedName)
104+
}
84105
}
85106
}
86107

@@ -105,6 +126,8 @@ func (h *Header) String() string {
105126
return h.Name + ";"
106127
case Add:
107128
return h.Name + ":" + *h.Value
129+
case RenameCase:
130+
return "%" + h.Name
108131
default:
109132
return ""
110133
}

header/header_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ import (
1111
"testing"
1212

1313
"github.com/google/go-cmp/cmp"
14+
"github.com/stretchr/testify/require"
1415
)
1516

1617
func TestParseHeader(t *testing.T) {
1718
tests := []struct {
1819
input string
1920
expected Header
2021
}{
22+
{
23+
input: "%rename-me",
24+
expected: Header{
25+
Name: "rename-me",
26+
Action: RenameCase,
27+
},
28+
},
2129
{
2230
input: "-RemoveMe",
2331
expected: Header{
@@ -160,3 +168,22 @@ func TestRemoveHeadersByPrefix(t *testing.T) {
160168
})
161169
}
162170
}
171+
172+
func TestReplaceCaseHeaderApply(t *testing.T) {
173+
// force header to be in non-canonicalised form according to pattern
174+
header := Header{
175+
Name: "rename-mE",
176+
Action: RenameCase,
177+
}
178+
179+
httpHeader := make(http.Header)
180+
181+
httpHeader.Add("Rename-Me", "true")
182+
header.Apply(httpHeader)
183+
184+
_, ok := httpHeader["rename-mE"] //nolint
185+
require.True(t, ok)
186+
187+
_, ok = httpHeader["Rename-Me"]
188+
require.False(t, ok)
189+
}

0 commit comments

Comments
 (0)