1package v2
2
3import (
4	"fmt"
5	"regexp"
6	"strings"
7	"unicode"
8)
9
10var (
11	// according to rfc7230
12	reToken            = regexp.MustCompile(`^[^"(),/:;<=>?@[\]{}[:space:][:cntrl:]]+`)
13	reQuotedValue      = regexp.MustCompile(`^[^\\"]+`)
14	reEscapedCharacter = regexp.MustCompile(`^[[:blank:][:graph:]]`)
15)
16
17// parseForwardedHeader is a benevolent parser of Forwarded header defined in rfc7239. The header contains
18// a comma-separated list of forwarding key-value pairs. Each list element is set by single proxy. The
19// function parses only the first element of the list, which is set by the very first proxy. It returns a map
20// of corresponding key-value pairs and an unparsed slice of the input string.
21//
22// Examples of Forwarded header values:
23//
24//  1. Forwarded: For=192.0.2.43; Proto=https,For="[2001:db8:cafe::17]",For=unknown
25//  2. Forwarded: for="192.0.2.43:443"; host="registry.example.org", for="10.10.05.40:80"
26//
27// The first will be parsed into {"for": "192.0.2.43", "proto": "https"} while the second into
28// {"for": "192.0.2.43:443", "host": "registry.example.org"}.
29func parseForwardedHeader(forwarded string) (map[string]string, string, error) {
30	// Following are states of forwarded header parser. Any state could transition to a failure.
31	const (
32		// terminating state; can transition to Parameter
33		stateElement = iota
34		// terminating state; can transition to KeyValueDelimiter
35		stateParameter
36		// can transition to Value
37		stateKeyValueDelimiter
38		// can transition to one of { QuotedValue, PairEnd }
39		stateValue
40		// can transition to one of { EscapedCharacter, PairEnd }
41		stateQuotedValue
42		// can transition to one of { QuotedValue }
43		stateEscapedCharacter
44		// terminating state; can transition to one of { Parameter, Element }
45		statePairEnd
46	)
47
48	var (
49		parameter string
50		value     string
51		parse     = forwarded[:]
52		res       = map[string]string{}
53		state     = stateElement
54	)
55
56Loop:
57	for {
58		// skip spaces unless in quoted value
59		if state != stateQuotedValue && state != stateEscapedCharacter {
60			parse = strings.TrimLeftFunc(parse, unicode.IsSpace)
61		}
62
63		if len(parse) == 0 {
64			if state != stateElement && state != statePairEnd && state != stateParameter {
65				return nil, parse, fmt.Errorf("unexpected end of input")
66			}
67			// terminating
68			break
69		}
70
71		switch state {
72		// terminate at list element delimiter
73		case stateElement:
74			if parse[0] == ',' {
75				parse = parse[1:]
76				break Loop
77			}
78			state = stateParameter
79
80		// parse parameter (the key of key-value pair)
81		case stateParameter:
82			match := reToken.FindString(parse)
83			if len(match) == 0 {
84				return nil, parse, fmt.Errorf("failed to parse token at position %d", len(forwarded)-len(parse))
85			}
86			parameter = strings.ToLower(match)
87			parse = parse[len(match):]
88			state = stateKeyValueDelimiter
89
90		// parse '='
91		case stateKeyValueDelimiter:
92			if parse[0] != '=' {
93				return nil, parse, fmt.Errorf("expected '=', not '%c' at position %d", parse[0], len(forwarded)-len(parse))
94			}
95			parse = parse[1:]
96			state = stateValue
97
98		// parse value or quoted value
99		case stateValue:
100			if parse[0] == '"' {
101				parse = parse[1:]
102				state = stateQuotedValue
103			} else {
104				value = reToken.FindString(parse)
105				if len(value) == 0 {
106					return nil, parse, fmt.Errorf("failed to parse value at position %d", len(forwarded)-len(parse))
107				}
108				if _, exists := res[parameter]; exists {
109					return nil, parse, fmt.Errorf("duplicate parameter %q at position %d", parameter, len(forwarded)-len(parse))
110				}
111				res[parameter] = value
112				parse = parse[len(value):]
113				value = ""
114				state = statePairEnd
115			}
116
117		// parse a part of quoted value until the first backslash
118		case stateQuotedValue:
119			match := reQuotedValue.FindString(parse)
120			value += match
121			parse = parse[len(match):]
122			switch {
123			case len(parse) == 0:
124				return nil, parse, fmt.Errorf("unterminated quoted string")
125			case parse[0] == '"':
126				res[parameter] = value
127				value = ""
128				parse = parse[1:]
129				state = statePairEnd
130			case parse[0] == '\\':
131				parse = parse[1:]
132				state = stateEscapedCharacter
133			}
134
135		// parse escaped character in a quoted string, ignore the backslash
136		// transition back to QuotedValue state
137		case stateEscapedCharacter:
138			c := reEscapedCharacter.FindString(parse)
139			if len(c) == 0 {
140				return nil, parse, fmt.Errorf("invalid escape sequence at position %d", len(forwarded)-len(parse)-1)
141			}
142			value += c
143			parse = parse[1:]
144			state = stateQuotedValue
145
146		// expect either a new key-value pair, new list or end of input
147		case statePairEnd:
148			switch parse[0] {
149			case ';':
150				parse = parse[1:]
151				state = stateParameter
152			case ',':
153				state = stateElement
154			default:
155				return nil, parse, fmt.Errorf("expected ',' or ';', not %c at position %d", parse[0], len(forwarded)-len(parse))
156			}
157		}
158	}
159
160	return res, parse, nil
161}
162