summaryrefslogtreecommitdiff
path: root/vendor/github.com/docker/distribution/registry/api/v2/headerparser.go
blob: 9bc41a3a64f994fb9d8fb71e84eed00bc696bc37 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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
}