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