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", | ||||||
|  | 			request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ | ||||||
|  | 				"X-Forwarded-Proto": []string{"https"}, | ||||||
|  | 			}}, | ||||||
| 			base: "http://example.com", | 			base: "http://example.com", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, | 			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", | 			base: "https://example.com", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader1}, | 			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", | 			base: "http://first.example.com", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2}, | 			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", | 			base: "http://first.example.com", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2}, | 			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", | 			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…
	
	Add table
		Add a link
		
	
		Reference in a new issue