Merge pull request #2008 from miminar/honor-x-forwarded-port
Honor X-Forwarded-Port header
This commit is contained in:
commit
fbe6e8d212
4 changed files with 698 additions and 24 deletions
161
registry/api/v2/headerparser.go
Normal file
161
registry/api/v2/headerparser.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// according to rfc7230
|
||||||
|
reToken = regexp.MustCompile(`^[^"(),/:;<=>?@[\]{}[:space:][:cntrl:]]+`)
|
||||||
|
reQuotedValue = regexp.MustCompile(`^[^\\"]+`)
|
||||||
|
reEscapedCharacter = regexp.MustCompile(`^[[:blank:][:graph:]]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseForwardedHeader is a benevolent parser of Forwarded header defined in rfc7239. The header contains
|
||||||
|
// a comma-separated list of forwarding key-value pairs. Each list element is set by single proxy. The
|
||||||
|
// function parses only the first element of the list, which is set by the very first proxy. It returns a map
|
||||||
|
// of corresponding key-value pairs and an unparsed slice of the input string.
|
||||||
|
//
|
||||||
|
// Examples of Forwarded header values:
|
||||||
|
//
|
||||||
|
// 1. Forwarded: For=192.0.2.43; Proto=https,For="[2001:db8:cafe::17]",For=unknown
|
||||||
|
// 2. Forwarded: for="192.0.2.43:443"; host="registry.example.org", for="10.10.05.40:80"
|
||||||
|
//
|
||||||
|
// The first will be parsed into {"for": "192.0.2.43", "proto": "https"} while the second into
|
||||||
|
// {"for": "192.0.2.43:443", "host": "registry.example.org"}.
|
||||||
|
func parseForwardedHeader(forwarded string) (map[string]string, string, error) {
|
||||||
|
// Following are states of forwarded header parser. Any state could transition to a failure.
|
||||||
|
const (
|
||||||
|
// terminating state; can transition to Parameter
|
||||||
|
stateElement = iota
|
||||||
|
// terminating state; can transition to KeyValueDelimiter
|
||||||
|
stateParameter
|
||||||
|
// can transition to Value
|
||||||
|
stateKeyValueDelimiter
|
||||||
|
// can transition to one of { QuotedValue, PairEnd }
|
||||||
|
stateValue
|
||||||
|
// can transition to one of { EscapedCharacter, PairEnd }
|
||||||
|
stateQuotedValue
|
||||||
|
// can transition to one of { QuotedValue }
|
||||||
|
stateEscapedCharacter
|
||||||
|
// terminating state; can transition to one of { Parameter, Element }
|
||||||
|
statePairEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
parameter string
|
||||||
|
value string
|
||||||
|
parse = forwarded[:]
|
||||||
|
res = map[string]string{}
|
||||||
|
state = stateElement
|
||||||
|
)
|
||||||
|
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
// skip spaces unless in quoted value
|
||||||
|
if state != stateQuotedValue && state != stateEscapedCharacter {
|
||||||
|
parse = strings.TrimLeftFunc(parse, unicode.IsSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parse) == 0 {
|
||||||
|
if state != stateElement && state != statePairEnd && state != stateParameter {
|
||||||
|
return nil, parse, fmt.Errorf("unexpected end of input")
|
||||||
|
}
|
||||||
|
// terminating
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
// terminate at list element delimiter
|
||||||
|
case stateElement:
|
||||||
|
if parse[0] == ',' {
|
||||||
|
parse = parse[1:]
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
state = stateParameter
|
||||||
|
|
||||||
|
// parse parameter (the key of key-value pair)
|
||||||
|
case stateParameter:
|
||||||
|
match := reToken.FindString(parse)
|
||||||
|
if len(match) == 0 {
|
||||||
|
return nil, parse, fmt.Errorf("failed to parse token at position %d", len(forwarded)-len(parse))
|
||||||
|
}
|
||||||
|
parameter = strings.ToLower(match)
|
||||||
|
parse = parse[len(match):]
|
||||||
|
state = stateKeyValueDelimiter
|
||||||
|
|
||||||
|
// parse '='
|
||||||
|
case stateKeyValueDelimiter:
|
||||||
|
if parse[0] != '=' {
|
||||||
|
return nil, parse, fmt.Errorf("expected '=', not '%c' at position %d", parse[0], len(forwarded)-len(parse))
|
||||||
|
}
|
||||||
|
parse = parse[1:]
|
||||||
|
state = stateValue
|
||||||
|
|
||||||
|
// parse value or quoted value
|
||||||
|
case stateValue:
|
||||||
|
if parse[0] == '"' {
|
||||||
|
parse = parse[1:]
|
||||||
|
state = stateQuotedValue
|
||||||
|
} else {
|
||||||
|
value = reToken.FindString(parse)
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, parse, fmt.Errorf("failed to parse value at position %d", len(forwarded)-len(parse))
|
||||||
|
}
|
||||||
|
if _, exists := res[parameter]; exists {
|
||||||
|
return nil, parse, fmt.Errorf("duplicate parameter %q at position %d", parameter, len(forwarded)-len(parse))
|
||||||
|
}
|
||||||
|
res[parameter] = value
|
||||||
|
parse = parse[len(value):]
|
||||||
|
value = ""
|
||||||
|
state = statePairEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a part of quoted value until the first backslash
|
||||||
|
case stateQuotedValue:
|
||||||
|
match := reQuotedValue.FindString(parse)
|
||||||
|
value += match
|
||||||
|
parse = parse[len(match):]
|
||||||
|
switch {
|
||||||
|
case len(parse) == 0:
|
||||||
|
return nil, parse, fmt.Errorf("unterminated quoted string")
|
||||||
|
case parse[0] == '"':
|
||||||
|
res[parameter] = value
|
||||||
|
value = ""
|
||||||
|
parse = parse[1:]
|
||||||
|
state = statePairEnd
|
||||||
|
case parse[0] == '\\':
|
||||||
|
parse = parse[1:]
|
||||||
|
state = stateEscapedCharacter
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse escaped character in a quoted string, ignore the backslash
|
||||||
|
// transition back to QuotedValue state
|
||||||
|
case stateEscapedCharacter:
|
||||||
|
c := reEscapedCharacter.FindString(parse)
|
||||||
|
if len(c) == 0 {
|
||||||
|
return nil, parse, fmt.Errorf("invalid escape sequence at position %d", len(forwarded)-len(parse)-1)
|
||||||
|
}
|
||||||
|
value += c
|
||||||
|
parse = parse[1:]
|
||||||
|
state = stateQuotedValue
|
||||||
|
|
||||||
|
// expect either a new key-value pair, new list or end of input
|
||||||
|
case statePairEnd:
|
||||||
|
switch parse[0] {
|
||||||
|
case ';':
|
||||||
|
parse = parse[1:]
|
||||||
|
state = stateParameter
|
||||||
|
case ',':
|
||||||
|
state = stateElement
|
||||||
|
default:
|
||||||
|
return nil, parse, fmt.Errorf("expected ',' or ';', not %c at position %d", parse[0], len(forwarded)-len(parse))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, parse, nil
|
||||||
|
}
|
161
registry/api/v2/headerparser_test.go
Normal file
161
registry/api/v2/headerparser_test.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseForwardedHeader(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
expected map[string]string
|
||||||
|
expectedRest string
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
raw: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one pair",
|
||||||
|
raw: " key = value ",
|
||||||
|
expected: map[string]string{"key": "value"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two pairs",
|
||||||
|
raw: " key1 = value1; key2=value2",
|
||||||
|
expected: map[string]string{"key1": "value1", "key2": "value2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "uppercase parameter",
|
||||||
|
raw: "KeY=VaL",
|
||||||
|
expected: map[string]string{"key": "VaL"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing key=value pair - be tolerant",
|
||||||
|
raw: "key=val;",
|
||||||
|
expected: map[string]string{"key": "val"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted values",
|
||||||
|
raw: `key="val";param = "[[ $((1 + 1)) == 3 ]] && echo panic!;" ; p=" abcd "`,
|
||||||
|
expected: map[string]string{"key": "val", "param": "[[ $((1 + 1)) == 3 ]] && echo panic!;", "p": " abcd "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty quoted value",
|
||||||
|
raw: `key=""`,
|
||||||
|
expected: map[string]string{"key": ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted double quotes",
|
||||||
|
raw: `key="\"value\""`,
|
||||||
|
expected: map[string]string{"key": `"value"`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted backslash",
|
||||||
|
raw: `key="\"\\\""`,
|
||||||
|
expected: map[string]string{"key": `"\"`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore subsequent elements",
|
||||||
|
raw: "key=a, param= b",
|
||||||
|
expected: map[string]string{"key": "a"},
|
||||||
|
expectedRest: " param= b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty element - be tolerant",
|
||||||
|
raw: " , key=val",
|
||||||
|
expectedRest: " key=val",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "obscure key",
|
||||||
|
raw: `ob₷C&r€ = value`,
|
||||||
|
expected: map[string]string{`ob₷c&r€`: "value"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate parameter",
|
||||||
|
raw: "key=a; p=b; key=c",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty parameter",
|
||||||
|
raw: "=value",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty value",
|
||||||
|
raw: "key= ",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty value before a new element ",
|
||||||
|
raw: "key=,",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty value before a new pair",
|
||||||
|
raw: "key=;",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just parameter",
|
||||||
|
raw: "key",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing key-value",
|
||||||
|
raw: "a=b;;",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unclosed quoted value",
|
||||||
|
raw: `key="value`,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped terminating dquote",
|
||||||
|
raw: `key="value\"`,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just a quoted value",
|
||||||
|
raw: `"key=val"`,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted key",
|
||||||
|
raw: `"key"=val`,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
parsed, rest, err := parseForwardedHeader(tc.raw)
|
||||||
|
if err != nil && !tc.expectedError {
|
||||||
|
t.Errorf("[%s] got unexpected error: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
if err == nil && tc.expectedError {
|
||||||
|
t.Errorf("[%s] got unexpected non-error", tc.name)
|
||||||
|
}
|
||||||
|
if err != nil || tc.expectedError {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for key, value := range tc.expected {
|
||||||
|
v, exists := parsed[key]
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("[%s] missing expected parameter %q", tc.name, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v != value {
|
||||||
|
t.Errorf("[%s] got unexpected value for parameter %q: %q != %q", tc.name, key, v, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, value := range parsed {
|
||||||
|
if _, exists := tc.expected[key]; !exists {
|
||||||
|
t.Errorf("[%s] got unexpected key/value pair: %q=%q", tc.name, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rest != tc.expectedRest {
|
||||||
|
t.Errorf("[%s] got unexpected unparsed string: %q != %q", tc.name, rest, tc.expectedRest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
|
@ -49,10 +51,14 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
||||||
var scheme string
|
var scheme string
|
||||||
|
|
||||||
forwardedProto := r.Header.Get("X-Forwarded-Proto")
|
forwardedProto := r.Header.Get("X-Forwarded-Proto")
|
||||||
|
// TODO: log the error
|
||||||
|
forwardedHeader, _, _ := parseForwardedHeader(r.Header.Get("Forwarded"))
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(forwardedProto) > 0:
|
case len(forwardedProto) > 0:
|
||||||
scheme = forwardedProto
|
scheme = forwardedProto
|
||||||
|
case len(forwardedHeader["proto"]) > 0:
|
||||||
|
scheme = forwardedHeader["proto"]
|
||||||
case r.TLS != nil:
|
case r.TLS != nil:
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
case len(r.URL.Scheme) > 0:
|
case len(r.URL.Scheme) > 0:
|
||||||
|
@ -62,14 +68,46 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
host := r.Host
|
host := r.Host
|
||||||
forwardedHost := r.Header.Get("X-Forwarded-Host")
|
|
||||||
if len(forwardedHost) > 0 {
|
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
|
||||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||||
// comma-separated list of hosts, to which each proxy appends the
|
// comma-separated list of hosts, to which each proxy appends the
|
||||||
// requested host. We want to grab the first from this comma-separated
|
// requested host. We want to grab the first from this comma-separated
|
||||||
// list.
|
// list.
|
||||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||||
host = strings.TrimSpace(hosts[0])
|
host = strings.TrimSpace(hosts[0])
|
||||||
|
} else if addr, exists := forwardedHeader["for"]; exists {
|
||||||
|
host = addr
|
||||||
|
} else if h, exists := forwardedHeader["host"]; exists {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
|
||||||
|
portLessHost, port := host, ""
|
||||||
|
if !isIPv6Address(portLessHost) {
|
||||||
|
// with go 1.6, this would treat the last part of IPv6 address as a port
|
||||||
|
portLessHost, port, _ = net.SplitHostPort(host)
|
||||||
|
}
|
||||||
|
if forwardedPort := r.Header.Get("X-Forwarded-Port"); len(port) == 0 && len(forwardedPort) > 0 {
|
||||||
|
ports := strings.SplitN(forwardedPort, ",", 2)
|
||||||
|
forwardedPort = strings.TrimSpace(ports[0])
|
||||||
|
if _, err := strconv.ParseInt(forwardedPort, 10, 32); err == nil {
|
||||||
|
port = forwardedPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(portLessHost) > 0 {
|
||||||
|
host = portLessHost
|
||||||
|
}
|
||||||
|
if len(port) > 0 {
|
||||||
|
// remove enclosing brackets of ipv6 address otherwise they will be duplicated
|
||||||
|
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||||
|
host = host[1 : len(host)-1]
|
||||||
|
}
|
||||||
|
// JoinHostPort properly encloses ipv6 addresses in square brackets
|
||||||
|
host = net.JoinHostPort(host, port)
|
||||||
|
} else if isIPv6Address(host) && host[0] != '[' {
|
||||||
|
// ipv6 needs to be enclosed in square brackets in urls
|
||||||
|
host = "[" + host + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
basePath := routeDescriptorsMap[RouteNameBase].Path
|
basePath := routeDescriptorsMap[RouteNameBase].Path
|
||||||
|
@ -249,3 +287,28 @@ func appendValues(u string, values ...url.Values) string {
|
||||||
|
|
||||||
return appendValuesURL(up, values...).String()
|
return appendValuesURL(up, values...).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isIPv6Address returns true if given string is a valid IPv6 address. No port is allowed. The address may be
|
||||||
|
// enclosed in square brackets.
|
||||||
|
func isIPv6Address(host string) bool {
|
||||||
|
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||||
|
host = host[1 : len(host)-1]
|
||||||
|
}
|
||||||
|
// The IPv6 scoped addressing zone identifier starts after the last percent sign.
|
||||||
|
if i := strings.LastIndexByte(host, '%'); i > 0 {
|
||||||
|
host = host[:i]
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ip.To16() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ip.To4() == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// dot can be present in ipv4-mapped address, it needs to come after a colon though
|
||||||
|
i := strings.IndexAny(host, ":.")
|
||||||
|
return i >= 0 && host[i] == ':'
|
||||||
|
}
|
||||||
|
|
|
@ -165,50 +165,222 @@ func TestBuilderFromRequest(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
forwardedProtoHeader := make(http.Header, 1)
|
|
||||||
forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
|
|
||||||
|
|
||||||
forwardedHostHeader1 := make(http.Header, 1)
|
|
||||||
forwardedHostHeader1.Set("X-Forwarded-Host", "first.example.com")
|
|
||||||
|
|
||||||
forwardedHostHeader2 := make(http.Header, 1)
|
|
||||||
forwardedHostHeader2.Set("X-Forwarded-Host", "first.example.com, proxy1.example.com")
|
|
||||||
|
|
||||||
testRequests := []struct {
|
testRequests := []struct {
|
||||||
|
name string
|
||||||
request *http.Request
|
request *http.Request
|
||||||
base string
|
base string
|
||||||
configHost url.URL
|
configHost url.URL
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "no forwarded header",
|
||||||
request: &http.Request{URL: u, Host: u.Host},
|
request: &http.Request{URL: u, Host: u.Host},
|
||||||
base: "http://example.com",
|
base: "http://example.com",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
|
name: "https protocol forwarded with a non-standard header",
|
||||||
base: "http://example.com",
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Proto": []string{"https"},
|
||||||
|
}},
|
||||||
|
base: "http://example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
|
name: "forwarded protocol is the same",
|
||||||
base: "https://example.com",
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Proto": []string{"https"},
|
||||||
|
}},
|
||||||
|
base: "https://example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader1},
|
name: "forwarded host with a non-standard header",
|
||||||
base: "http://first.example.com",
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"first.example.com"},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2},
|
name: "forwarded multiple hosts a with non-standard header",
|
||||||
base: "http://first.example.com",
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2},
|
name: "host configured in config file takes priority",
|
||||||
base: "https://third.example.com:5000",
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"},
|
||||||
|
}},
|
||||||
|
base: "https://third.example.com:5000",
|
||||||
configHost: url.URL{
|
configHost: url.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Host: "third.example.com:5000",
|
Host: "third.example.com:5000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded host and port with just one non-standard header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"first.example.com:443"},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded port with a non-standard header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Port": []string{"5000"},
|
||||||
|
}},
|
||||||
|
base: "http://example.com:5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded multiple ports with a non-standard header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Port": []string{"443 , 5001"},
|
||||||
|
}},
|
||||||
|
base: "http://example.com:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "several non-standard headers",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Proto": []string{"https"},
|
||||||
|
"X-Forwarded-Host": []string{" first.example.com "},
|
||||||
|
"X-Forwarded-Port": []string{" 12345 \t"},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com:12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded host with port supplied takes priority",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"first.example.com:5000"},
|
||||||
|
"X-Forwarded-Port": []string{"80"},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com:5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed forwarded port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"first.example.com"},
|
||||||
|
"X-Forwarded-Port": []string{"abcd"},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded protocol and addr using standard header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`proto=https;for="192.168.22.30:80"`},
|
||||||
|
}},
|
||||||
|
base: "https://192.168.22.30:80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded addr takes priority over host",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`host=reg.example.com;for="192.168.22.30:5000"`},
|
||||||
|
}},
|
||||||
|
base: "http://192.168.22.30:5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forwarded host and protocol using standard header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`host=reg.example.com;proto=https`},
|
||||||
|
}},
|
||||||
|
base: "https://reg.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "process just the first standard forwarded header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`host="reg.example.com:88";proto=http`, `host=reg.example.com;proto=https`},
|
||||||
|
}},
|
||||||
|
base: "http://reg.example.com:88",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "process just the first list element of standard header",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="reg.example.com:443";proto=https, for="reg.example.com:80";proto=http`},
|
||||||
|
}},
|
||||||
|
base: "https://reg.example.com:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address override port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="2607:f0d0:1002:51::4"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[2607:f0d0:1002:51::4]:5001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address with port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="[2607:f0d0:1002:51::4]:4000"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[2607:f0d0:1002:51::4]:4000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 long address override port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="2607:f0d0:1002:0051:0000:0000:0000:0004"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:5001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 long address enclosed in brackets - be benevolent",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="[2607:f0d0:1002:0051:0000:0000:0000:0004]"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:5001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 long address with port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address with zone ID",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="fe80::bd0f:a8bc:6480:238b%11"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[fe80::bd0f:a8bc:6480:238b%2511]:5001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address with zone ID and port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="[fe80::bd0f:a8bc:6480:238b%eth0]:12345"`},
|
||||||
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
|
}},
|
||||||
|
base: "http://[fe80::bd0f:a8bc:6480:238b%25eth0]:12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address without port",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"Forwarded": []string{`for="::FFFF:129.144.52.38"`},
|
||||||
|
}},
|
||||||
|
base: "http://[::FFFF:129.144.52.38]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-standard and standard forward headers",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Proto": []string{`https`},
|
||||||
|
"X-Forwarded-Host": []string{`first.example.com`},
|
||||||
|
"X-Forwarded-Port": []string{``},
|
||||||
|
"Forwarded": []string{`host=first.example.com; proto=https`},
|
||||||
|
}},
|
||||||
|
base: "https://first.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-standard headers take precedence over standard one",
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Proto": []string{`http`},
|
||||||
|
"Forwarded": []string{`host=second.example.com; proto=https`},
|
||||||
|
"X-Forwarded-Host": []string{`first.example.com`},
|
||||||
|
"X-Forwarded-Port": []string{`4000`},
|
||||||
|
}},
|
||||||
|
base: "http://first.example.com:4000",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
doTest := func(relative bool) {
|
doTest := func(relative bool) {
|
||||||
for _, tr := range testRequests {
|
for _, tr := range testRequests {
|
||||||
var builder *URLBuilder
|
var builder *URLBuilder
|
||||||
|
@ -221,7 +393,7 @@ func TestBuilderFromRequest(t *testing.T) {
|
||||||
for _, testCase := range makeURLBuilderTestCases(builder) {
|
for _, testCase := range makeURLBuilderTestCases(builder) {
|
||||||
buildURL, err := testCase.build()
|
buildURL, err := testCase.build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s: error building url: %v", testCase.description, err)
|
t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedURL string
|
var expectedURL string
|
||||||
|
@ -244,11 +416,12 @@ func TestBuilderFromRequest(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if buildURL != expectedURL {
|
if buildURL != expectedURL {
|
||||||
t.Fatalf("%s: %q != %q", testCase.description, buildURL, expectedURL)
|
t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, testCase.description, buildURL, expectedURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doTest(true)
|
doTest(true)
|
||||||
doTest(false)
|
doTest(false)
|
||||||
}
|
}
|
||||||
|
@ -332,3 +505,119 @@ func TestBuilderFromRequestWithPrefix(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsIPv6Address(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
address string
|
||||||
|
isIPv6 bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "IPv6 short address",
|
||||||
|
address: `2607:f0d0:1002:51::4`,
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 short address enclosed in brackets",
|
||||||
|
address: "[2607:f0d0:1002:51::4]",
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address",
|
||||||
|
address: `2607:f0d0:1002:0051:0000:0000:0000:0004`,
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address with numeric zone ID",
|
||||||
|
address: `fe80::bd0f:a8bc:6480:238b%11`,
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address with device name as zone ID",
|
||||||
|
address: `fe80::bd0f:a8bc:6480:238b%eth0`,
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address with device name as zone ID enclosed in brackets",
|
||||||
|
address: `[fe80::bd0f:a8bc:6480:238b%eth0]`,
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4-mapped address",
|
||||||
|
address: "::FFFF:129.144.52.38",
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "localhost",
|
||||||
|
address: "::1",
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "localhost",
|
||||||
|
address: "::1",
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long localhost address",
|
||||||
|
address: "0:0:0:0:0:0:0:1",
|
||||||
|
isIPv6: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 long address with port",
|
||||||
|
address: "[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321",
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many groups",
|
||||||
|
address: "2607:f0d0:1002:0051:0000:0000:0000:0004:4321",
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "square brackets don't make an IPv6 address",
|
||||||
|
address: "[2607:f0d0]",
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "require two consecutive colons in localhost",
|
||||||
|
address: ":1",
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "more then 4 hexadecimal digits",
|
||||||
|
address: "2607:f0d0b:1002:0051:0000:0000:0000:0004",
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too short address",
|
||||||
|
address: `2607:f0d0:1002:0000:0000:0000:0004`,
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 address",
|
||||||
|
address: `192.168.100.1`,
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unclosed bracket",
|
||||||
|
address: `[2607:f0d0:1002:0051:0000:0000:0000:0004`,
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing bracket",
|
||||||
|
address: `2607:f0d0:1002:0051:0000:0000:0000:0004]`,
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "domain name",
|
||||||
|
address: `localhost`,
|
||||||
|
isIPv6: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
isIPv6 := isIPv6Address(tc.address)
|
||||||
|
if isIPv6 && !tc.isIPv6 {
|
||||||
|
t.Errorf("[%s] address %q falsely detected as IPv6 address", tc.name, tc.address)
|
||||||
|
} else if !isIPv6 && tc.isIPv6 {
|
||||||
|
t.Errorf("[%s] address %q not recognized as IPv6", tc.name, tc.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue