Honor X-Forwarded-Port and Forwarded headers
Prefer non-standard headers like X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Port over the standard Forwarded header to maintain backwards compatibility. If a port is not specified neither in Host nor in forwarded headers but it is specified just with X-Forwarded-Port, use its value in base urls for redirects. Forwarded header is defined in rfc7239. X-Forwarded-Port is a non-standard header. Here's a description copied from "HTTP Headers and Elastic Load Balancing" of AWS ELB docs: > The X-Forwarded-Port request header helps you identify the port that > an HTTP or HTTPS load balancer uses to connect to the client. Signed-off-by: Michal Minář <miminar@redhat.com>master
							parent
							
								
									d0cdc4802b
								
							
						
					
					
						commit
						1b43e1e30d
					
				|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| 
 | ||||
| import ( | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/docker/distribution/reference" | ||||
|  | @ -49,10 +51,14 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder { | |||
| 	var scheme string | ||||
| 
 | ||||
| 	forwardedProto := r.Header.Get("X-Forwarded-Proto") | ||||
| 	// TODO: log the error
 | ||||
| 	forwardedHeader, _, _ := parseForwardedHeader(r.Header.Get("Forwarded")) | ||||
| 
 | ||||
| 	switch { | ||||
| 	case len(forwardedProto) > 0: | ||||
| 		scheme = forwardedProto | ||||
| 	case len(forwardedHeader["proto"]) > 0: | ||||
| 		scheme = forwardedHeader["proto"] | ||||
| 	case r.TLS != nil: | ||||
| 		scheme = "https" | ||||
| 	case len(r.URL.Scheme) > 0: | ||||
|  | @ -62,14 +68,46 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder { | |||
| 	} | ||||
| 
 | ||||
| 	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
 | ||||
| 		// comma-separated list of hosts, to which each proxy appends the
 | ||||
| 		// requested host. We want to grab the first from this comma-separated
 | ||||
| 		// list.
 | ||||
| 		hosts := strings.SplitN(forwardedHost, ",", 2) | ||||
| 		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 | ||||
|  | @ -249,3 +287,28 @@ func appendValues(u string, values ...url.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) | ||||
| 	} | ||||
| 
 | ||||
| 	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 { | ||||
| 		name       string | ||||
| 		request    *http.Request | ||||
| 		base       string | ||||
| 		configHost url.URL | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:    "no forwarded header", | ||||
| 			request: &http.Request{URL: u, Host: u.Host}, | ||||
| 			base:    "http://example.com", | ||||
| 		}, | ||||
| 
 | ||||
| 		{ | ||||
| 			request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, | ||||
| 			base:    "http://example.com", | ||||
| 			name: "https protocol forwarded with a non-standard header", | ||||
| 			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}, | ||||
| 			base:    "https://example.com", | ||||
| 			name: "forwarded protocol is the same", | ||||
| 			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}, | ||||
| 			base:    "http://first.example.com", | ||||
| 			name: "forwarded host with a non-standard header", | ||||
| 			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}, | ||||
| 			base:    "http://first.example.com", | ||||
| 			name: "forwarded multiple hosts a with non-standard header", | ||||
| 			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}, | ||||
| 			base:    "https://third.example.com:5000", | ||||
| 			name: "host configured in config file takes priority", | ||||
| 			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{ | ||||
| 				Scheme: "https", | ||||
| 				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) { | ||||
| 		for _, tr := range testRequests { | ||||
| 			var builder *URLBuilder | ||||
|  | @ -221,7 +393,7 @@ func TestBuilderFromRequest(t *testing.T) { | |||
| 			for _, testCase := range makeURLBuilderTestCases(builder) { | ||||
| 				buildURL, err := testCase.build() | ||||
| 				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 | ||||
|  | @ -244,11 +416,12 @@ func TestBuilderFromRequest(t *testing.T) { | |||
| 				} | ||||
| 
 | ||||
| 				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(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 New Issue