1b43e1e30d
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>
162 lines
4.7 KiB
Go
162 lines
4.7 KiB
Go
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
|
|
}
|