| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- package httpcc
- import (
- "bufio"
- "fmt"
- "strconv"
- "strings"
- "unicode/utf8"
- )
- const (
- // Request Cache-Control directives
- MaxAge = "max-age" // used in response as well
- MaxStale = "max-stale"
- MinFresh = "min-fresh"
- NoCache = "no-cache" // used in response as well
- NoStore = "no-store" // used in response as well
- NoTransform = "no-transform" // used in response as well
- OnlyIfCached = "only-if-cached"
- // Response Cache-Control directive
- MustRevalidate = "must-revalidate"
- Public = "public"
- Private = "private"
- ProxyRevalidate = "proxy-revalidate"
- SMaxAge = "s-maxage"
- )
- type TokenPair struct {
- Name string
- Value string
- }
- type TokenValuePolicy int
- const (
- NoArgument TokenValuePolicy = iota
- TokenOnly
- QuotedStringOnly
- AnyTokenValue
- )
- type directiveValidator interface {
- Validate(string) TokenValuePolicy
- }
- type directiveValidatorFn func(string) TokenValuePolicy
- func (fn directiveValidatorFn) Validate(ccd string) TokenValuePolicy {
- return fn(ccd)
- }
- func responseDirectiveValidator(s string) TokenValuePolicy {
- switch s {
- case MustRevalidate, NoStore, NoTransform, Public, ProxyRevalidate:
- return NoArgument
- case NoCache, Private:
- return QuotedStringOnly
- case MaxAge, SMaxAge:
- return TokenOnly
- default:
- return AnyTokenValue
- }
- }
- func requestDirectiveValidator(s string) TokenValuePolicy {
- switch s {
- case MaxAge, MaxStale, MinFresh:
- return TokenOnly
- case NoCache, NoStore, NoTransform, OnlyIfCached:
- return NoArgument
- default:
- return AnyTokenValue
- }
- }
- // ParseRequestDirective parses a single token.
- func ParseRequestDirective(s string) (*TokenPair, error) {
- return parseDirective(s, directiveValidatorFn(requestDirectiveValidator))
- }
- func ParseResponseDirective(s string) (*TokenPair, error) {
- return parseDirective(s, directiveValidatorFn(responseDirectiveValidator))
- }
- func parseDirective(s string, ccd directiveValidator) (*TokenPair, error) {
- s = strings.TrimSpace(s)
- i := strings.IndexByte(s, '=')
- if i == -1 {
- return &TokenPair{Name: s}, nil
- }
- pair := &TokenPair{Name: strings.TrimSpace(s[:i])}
- if len(s) <= i {
- // `key=` feels like it's a parse error, but it's HTTP...
- // for now, return as if nothing happened.
- return pair, nil
- }
- v := strings.TrimSpace(s[i+1:])
- switch ccd.Validate(pair.Name) {
- case TokenOnly:
- if v[0] == '"' {
- return nil, fmt.Errorf(`invalid value for %s (quoted string not allowed)`, pair.Name)
- }
- case QuotedStringOnly: // quoted-string only
- if v[0] != '"' {
- return nil, fmt.Errorf(`invalid value for %s (bare token not allowed)`, pair.Name)
- }
- tmp, err := strconv.Unquote(v)
- if err != nil {
- return nil, fmt.Errorf(`malformed quoted string in token`)
- }
- v = tmp
- case AnyTokenValue:
- if v[0] == '"' {
- tmp, err := strconv.Unquote(v)
- if err != nil {
- return nil, fmt.Errorf(`malformed quoted string in token`)
- }
- v = tmp
- }
- case NoArgument:
- if len(v) > 0 {
- return nil, fmt.Errorf(`received argument to directive %s`, pair.Name)
- }
- }
- pair.Value = v
- return pair, nil
- }
- func ParseResponseDirectives(s string) ([]*TokenPair, error) {
- return parseDirectives(s, ParseResponseDirective)
- }
- func ParseRequestDirectives(s string) ([]*TokenPair, error) {
- return parseDirectives(s, ParseRequestDirective)
- }
- func parseDirectives(s string, p func(string) (*TokenPair, error)) ([]*TokenPair, error) {
- scanner := bufio.NewScanner(strings.NewReader(s))
- scanner.Split(scanCommaSeparatedWords)
- var tokens []*TokenPair
- for scanner.Scan() {
- tok, err := p(scanner.Text())
- if err != nil {
- return nil, fmt.Errorf(`failed to parse token #%d: %w`, len(tokens)+1, err)
- }
- tokens = append(tokens, tok)
- }
- return tokens, nil
- }
- // isSpace reports whether the character is a Unicode white space character.
- // We avoid dependency on the unicode package, but check validity of the implementation
- // in the tests.
- func isSpace(r rune) bool {
- if r <= '\u00FF' {
- // Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs.
- switch r {
- case ' ', '\t', '\n', '\v', '\f', '\r':
- return true
- case '\u0085', '\u00A0':
- return true
- }
- return false
- }
- // High-valued ones.
- if '\u2000' <= r && r <= '\u200a' {
- return true
- }
- switch r {
- case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
- return true
- }
- return false
- }
- func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
- // Skip leading spaces.
- start := 0
- for width := 0; start < len(data); start += width {
- var r rune
- r, width = utf8.DecodeRune(data[start:])
- if !isSpace(r) {
- break
- }
- }
- // Scan until we find a comma. Keep track of consecutive whitespaces
- // so we remove them from the end result
- var ws int
- for width, i := 0, start; i < len(data); i += width {
- var r rune
- r, width = utf8.DecodeRune(data[i:])
- switch {
- case isSpace(r):
- ws++
- case r == ',':
- return i + width, data[start : i-ws], nil
- default:
- ws = 0
- }
- }
- // If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
- if atEOF && len(data) > start {
- return len(data), data[start : len(data)-ws], nil
- }
- // Request more data.
- return start, nil, nil
- }
- // ParseRequest parses the content of `Cache-Control` header of an HTTP Request.
- func ParseRequest(v string) (*RequestDirective, error) {
- var dir RequestDirective
- tokens, err := ParseRequestDirectives(v)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
- }
- for _, token := range tokens {
- name := strings.ToLower(token.Name)
- switch name {
- case MaxAge:
- iv, err := strconv.ParseUint(token.Value, 10, 64)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
- }
- dir.maxAge = &iv
- case MaxStale:
- iv, err := strconv.ParseUint(token.Value, 10, 64)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse max-stale: %w`, err)
- }
- dir.maxStale = &iv
- case MinFresh:
- iv, err := strconv.ParseUint(token.Value, 10, 64)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse min-fresh: %w`, err)
- }
- dir.minFresh = &iv
- case NoCache:
- dir.noCache = true
- case NoStore:
- dir.noStore = true
- case NoTransform:
- dir.noTransform = true
- case OnlyIfCached:
- dir.onlyIfCached = true
- default:
- dir.extensions[token.Name] = token.Value
- }
- }
- return &dir, nil
- }
- // ParseResponse parses the content of `Cache-Control` header of an HTTP Response.
- func ParseResponse(v string) (*ResponseDirective, error) {
- tokens, err := ParseResponseDirectives(v)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
- }
- var dir ResponseDirective
- dir.extensions = make(map[string]string)
- for _, token := range tokens {
- name := strings.ToLower(token.Name)
- switch name {
- case MaxAge:
- iv, err := strconv.ParseUint(token.Value, 10, 64)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
- }
- dir.maxAge = &iv
- case NoCache:
- scanner := bufio.NewScanner(strings.NewReader(token.Value))
- scanner.Split(scanCommaSeparatedWords)
- for scanner.Scan() {
- dir.noCache = append(dir.noCache, scanner.Text())
- }
- case NoStore:
- dir.noStore = true
- case NoTransform:
- dir.noTransform = true
- case Public:
- dir.public = true
- case Private:
- scanner := bufio.NewScanner(strings.NewReader(token.Value))
- scanner.Split(scanCommaSeparatedWords)
- for scanner.Scan() {
- dir.private = append(dir.private, scanner.Text())
- }
- case ProxyRevalidate:
- dir.proxyRevalidate = true
- case SMaxAge:
- iv, err := strconv.ParseUint(token.Value, 10, 64)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse s-maxage: %w`, err)
- }
- dir.sMaxAge = &iv
- default:
- dir.extensions[token.Name] = token.Value
- }
- }
- return &dir, nil
- }
|