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