httpcc.go 7.6 KB

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