...

Source file src/github.com/lestrrat-go/httpcc/httpcc.go

Documentation: github.com/lestrrat-go/httpcc

     1  package httpcc
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  	"unicode/utf8"
     9  )
    10  
    11  const (
    12  	// Request Cache-Control directives
    13  	MaxAge       = "max-age" // used in response as well
    14  	MaxStale     = "max-stale"
    15  	MinFresh     = "min-fresh"
    16  	NoCache      = "no-cache"     // used in response as well
    17  	NoStore      = "no-store"     // used in response as well
    18  	NoTransform  = "no-transform" // used in response as well
    19  	OnlyIfCached = "only-if-cached"
    20  
    21  	// Response Cache-Control directive
    22  	MustRevalidate  = "must-revalidate"
    23  	Public          = "public"
    24  	Private         = "private"
    25  	ProxyRevalidate = "proxy-revalidate"
    26  	SMaxAge         = "s-maxage"
    27  )
    28  
    29  type TokenPair struct {
    30  	Name  string
    31  	Value string
    32  }
    33  
    34  type TokenValuePolicy int
    35  
    36  const (
    37  	NoArgument TokenValuePolicy = iota
    38  	TokenOnly
    39  	QuotedStringOnly
    40  	AnyTokenValue
    41  )
    42  
    43  type directiveValidator interface {
    44  	Validate(string) TokenValuePolicy
    45  }
    46  type directiveValidatorFn func(string) TokenValuePolicy
    47  
    48  func (fn directiveValidatorFn) Validate(ccd string) TokenValuePolicy {
    49  	return fn(ccd)
    50  }
    51  
    52  func responseDirectiveValidator(s string) TokenValuePolicy {
    53  	switch s {
    54  	case MustRevalidate, NoStore, NoTransform, Public, ProxyRevalidate:
    55  		return NoArgument
    56  	case NoCache, Private:
    57  		return QuotedStringOnly
    58  	case MaxAge, SMaxAge:
    59  		return TokenOnly
    60  	default:
    61  		return AnyTokenValue
    62  	}
    63  }
    64  
    65  func requestDirectiveValidator(s string) TokenValuePolicy {
    66  	switch s {
    67  	case MaxAge, MaxStale, MinFresh:
    68  		return TokenOnly
    69  	case NoCache, NoStore, NoTransform, OnlyIfCached:
    70  		return NoArgument
    71  	default:
    72  		return AnyTokenValue
    73  	}
    74  }
    75  
    76  // ParseRequestDirective parses a single token.
    77  func ParseRequestDirective(s string) (*TokenPair, error) {
    78  	return parseDirective(s, directiveValidatorFn(requestDirectiveValidator))
    79  }
    80  
    81  func ParseResponseDirective(s string) (*TokenPair, error) {
    82  	return parseDirective(s, directiveValidatorFn(responseDirectiveValidator))
    83  }
    84  
    85  func parseDirective(s string, ccd directiveValidator) (*TokenPair, error) {
    86  	s = strings.TrimSpace(s)
    87  
    88  	i := strings.IndexByte(s, '=')
    89  	if i == -1 {
    90  		return &TokenPair{Name: s}, nil
    91  	}
    92  
    93  	pair := &TokenPair{Name: strings.TrimSpace(s[:i])}
    94  
    95  	if len(s) <= i {
    96  		// `key=` feels like it's a parse error, but it's HTTP...
    97  		// for now, return as if nothing happened.
    98  		return pair, nil
    99  	}
   100  
   101  	v := strings.TrimSpace(s[i+1:])
   102  	switch ccd.Validate(pair.Name) {
   103  	case TokenOnly:
   104  		if v[0] == '"' {
   105  			return nil, fmt.Errorf(`invalid value for %s (quoted string not allowed)`, pair.Name)
   106  		}
   107  	case QuotedStringOnly: // quoted-string only
   108  		if v[0] != '"' {
   109  			return nil, fmt.Errorf(`invalid value for %s (bare token not allowed)`, pair.Name)
   110  		}
   111  		tmp, err := strconv.Unquote(v)
   112  		if err != nil {
   113  			return nil, fmt.Errorf(`malformed quoted string in token`)
   114  		}
   115  		v = tmp
   116  	case AnyTokenValue:
   117  		if v[0] == '"' {
   118  			tmp, err := strconv.Unquote(v)
   119  			if err != nil {
   120  				return nil, fmt.Errorf(`malformed quoted string in token`)
   121  			}
   122  			v = tmp
   123  		}
   124  	case NoArgument:
   125  		if len(v) > 0 {
   126  			return nil, fmt.Errorf(`received argument to directive %s`, pair.Name)
   127  		}
   128  	}
   129  
   130  	pair.Value = v
   131  	return pair, nil
   132  }
   133  
   134  func ParseResponseDirectives(s string) ([]*TokenPair, error) {
   135  	return parseDirectives(s, ParseResponseDirective)
   136  }
   137  
   138  func ParseRequestDirectives(s string) ([]*TokenPair, error) {
   139  	return parseDirectives(s, ParseRequestDirective)
   140  }
   141  
   142  func parseDirectives(s string, p func(string) (*TokenPair, error)) ([]*TokenPair, error) {
   143  	scanner := bufio.NewScanner(strings.NewReader(s))
   144  	scanner.Split(scanCommaSeparatedWords)
   145  
   146  	var tokens []*TokenPair
   147  	for scanner.Scan() {
   148  		tok, err := p(scanner.Text())
   149  		if err != nil {
   150  			return nil, fmt.Errorf(`failed to parse token #%d: %w`, len(tokens)+1, err)
   151  		}
   152  		tokens = append(tokens, tok)
   153  	}
   154  	return tokens, nil
   155  }
   156  
   157  // isSpace reports whether the character is a Unicode white space character.
   158  // We avoid dependency on the unicode package, but check validity of the implementation
   159  // in the tests.
   160  func isSpace(r rune) bool {
   161  	if r <= '\u00FF' {
   162  		// Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs.
   163  		switch r {
   164  		case ' ', '\t', '\n', '\v', '\f', '\r':
   165  			return true
   166  		case '\u0085', '\u00A0':
   167  			return true
   168  		}
   169  		return false
   170  	}
   171  	// High-valued ones.
   172  	if '\u2000' <= r && r <= '\u200a' {
   173  		return true
   174  	}
   175  	switch r {
   176  	case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
   177  		return true
   178  	}
   179  	return false
   180  }
   181  
   182  func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
   183  	// Skip leading spaces.
   184  	start := 0
   185  	for width := 0; start < len(data); start += width {
   186  		var r rune
   187  		r, width = utf8.DecodeRune(data[start:])
   188  		if !isSpace(r) {
   189  			break
   190  		}
   191  	}
   192  	// Scan until we find a comma. Keep track of consecutive whitespaces
   193  	// so we remove them from the end result
   194  	var ws int
   195  	for width, i := 0, start; i < len(data); i += width {
   196  		var r rune
   197  		r, width = utf8.DecodeRune(data[i:])
   198  		switch {
   199  		case isSpace(r):
   200  			ws++
   201  		case r == ',':
   202  			return i + width, data[start : i-ws], nil
   203  		default:
   204  			ws = 0
   205  		}
   206  	}
   207  
   208  	// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
   209  	if atEOF && len(data) > start {
   210  		return len(data), data[start : len(data)-ws], nil
   211  	}
   212  
   213  	// Request more data.
   214  	return start, nil, nil
   215  }
   216  
   217  // ParseRequest parses the content of `Cache-Control` header of an HTTP Request.
   218  func ParseRequest(v string) (*RequestDirective, error) {
   219  	var dir RequestDirective
   220  	tokens, err := ParseRequestDirectives(v)
   221  	if err != nil {
   222  		return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
   223  	}
   224  
   225  	for _, token := range tokens {
   226  		name := strings.ToLower(token.Name)
   227  		switch name {
   228  		case MaxAge:
   229  			iv, err := strconv.ParseUint(token.Value, 10, 64)
   230  			if err != nil {
   231  				return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
   232  			}
   233  			dir.maxAge = &iv
   234  		case MaxStale:
   235  			iv, err := strconv.ParseUint(token.Value, 10, 64)
   236  			if err != nil {
   237  				return nil, fmt.Errorf(`failed to parse max-stale: %w`, err)
   238  			}
   239  			dir.maxStale = &iv
   240  		case MinFresh:
   241  			iv, err := strconv.ParseUint(token.Value, 10, 64)
   242  			if err != nil {
   243  				return nil, fmt.Errorf(`failed to parse min-fresh: %w`, err)
   244  			}
   245  			dir.minFresh = &iv
   246  		case NoCache:
   247  			dir.noCache = true
   248  		case NoStore:
   249  			dir.noStore = true
   250  		case NoTransform:
   251  			dir.noTransform = true
   252  		case OnlyIfCached:
   253  			dir.onlyIfCached = true
   254  		default:
   255  			dir.extensions[token.Name] = token.Value
   256  		}
   257  	}
   258  	return &dir, nil
   259  }
   260  
   261  // ParseResponse parses the content of `Cache-Control` header of an HTTP Response.
   262  func ParseResponse(v string) (*ResponseDirective, error) {
   263  	tokens, err := ParseResponseDirectives(v)
   264  	if err != nil {
   265  		return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
   266  	}
   267  
   268  	var dir ResponseDirective
   269  	dir.extensions = make(map[string]string)
   270  	for _, token := range tokens {
   271  		name := strings.ToLower(token.Name)
   272  		switch name {
   273  		case MaxAge:
   274  			iv, err := strconv.ParseUint(token.Value, 10, 64)
   275  			if err != nil {
   276  				return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
   277  			}
   278  			dir.maxAge = &iv
   279  		case NoCache:
   280  			scanner := bufio.NewScanner(strings.NewReader(token.Value))
   281  			scanner.Split(scanCommaSeparatedWords)
   282  			for scanner.Scan() {
   283  				dir.noCache = append(dir.noCache, scanner.Text())
   284  			}
   285  		case NoStore:
   286  			dir.noStore = true
   287  		case NoTransform:
   288  			dir.noTransform = true
   289  		case Public:
   290  			dir.public = true
   291  		case Private:
   292  			scanner := bufio.NewScanner(strings.NewReader(token.Value))
   293  			scanner.Split(scanCommaSeparatedWords)
   294  			for scanner.Scan() {
   295  				dir.private = append(dir.private, scanner.Text())
   296  			}
   297  		case ProxyRevalidate:
   298  			dir.proxyRevalidate = true
   299  		case SMaxAge:
   300  			iv, err := strconv.ParseUint(token.Value, 10, 64)
   301  			if err != nil {
   302  				return nil, fmt.Errorf(`failed to parse s-maxage: %w`, err)
   303  			}
   304  			dir.sMaxAge = &iv
   305  		default:
   306  			dir.extensions[token.Name] = token.Value
   307  		}
   308  	}
   309  	return &dir, nil
   310  }
   311  

View as plain text