mirror of https://github.com/docker/cli.git
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
|
||
|
}
|