|
@@ -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
|
|
|
+}
|