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