directive.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. /**
  2. * Copyright 2015 Paul Querna
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package cacheobject
  18. import (
  19. "errors"
  20. "math"
  21. "net/http"
  22. "net/textproto"
  23. "strconv"
  24. "strings"
  25. )
  26. // TODO(pquerna): add extensions from here: http://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
  27. var (
  28. ErrQuoteMismatch = errors.New("Missing closing quote")
  29. ErrMaxAgeDeltaSeconds = errors.New("Failed to parse delta-seconds in `max-age`")
  30. ErrSMaxAgeDeltaSeconds = errors.New("Failed to parse delta-seconds in `s-maxage`")
  31. ErrMaxStaleDeltaSeconds = errors.New("Failed to parse delta-seconds in `min-fresh`")
  32. ErrMinFreshDeltaSeconds = errors.New("Failed to parse delta-seconds in `min-fresh`")
  33. ErrNoCacheNoArgs = errors.New("Unexpected argument to `no-cache`")
  34. ErrNoStoreNoArgs = errors.New("Unexpected argument to `no-store`")
  35. ErrNoTransformNoArgs = errors.New("Unexpected argument to `no-transform`")
  36. ErrOnlyIfCachedNoArgs = errors.New("Unexpected argument to `only-if-cached`")
  37. ErrMustRevalidateNoArgs = errors.New("Unexpected argument to `must-revalidate`")
  38. ErrPublicNoArgs = errors.New("Unexpected argument to `public`")
  39. ErrProxyRevalidateNoArgs = errors.New("Unexpected argument to `proxy-revalidate`")
  40. // Experimental
  41. ErrImmutableNoArgs = errors.New("Unexpected argument to `immutable`")
  42. ErrStaleIfErrorDeltaSeconds = errors.New("Failed to parse delta-seconds in `stale-if-error`")
  43. ErrStaleWhileRevalidateDeltaSeconds = errors.New("Failed to parse delta-seconds in `stale-while-revalidate`")
  44. )
  45. func whitespace(b byte) bool {
  46. if b == '\t' || b == ' ' {
  47. return true
  48. }
  49. return false
  50. }
  51. func parse(value string, cd cacheDirective) error {
  52. var err error = nil
  53. i := 0
  54. for i < len(value) && err == nil {
  55. // eat leading whitespace or commas
  56. if whitespace(value[i]) || value[i] == ',' {
  57. i++
  58. continue
  59. }
  60. j := i + 1
  61. for j < len(value) {
  62. if !isToken(value[j]) {
  63. break
  64. }
  65. j++
  66. }
  67. token := strings.ToLower(value[i:j])
  68. tokenHasFields := hasFieldNames(token)
  69. /*
  70. println("GOT TOKEN:")
  71. println(" i -> ", i)
  72. println(" j -> ", j)
  73. println(" token -> ", token)
  74. */
  75. if j+1 < len(value) && value[j] == '=' {
  76. k := j + 1
  77. // minimum size two bytes of "", but we let httpUnquote handle it.
  78. if k < len(value) && value[k] == '"' {
  79. eaten, result := httpUnquote(value[k:])
  80. if eaten == -1 {
  81. return ErrQuoteMismatch
  82. }
  83. i = k + eaten
  84. err = cd.addPair(token, result)
  85. } else {
  86. z := k
  87. for z < len(value) {
  88. if tokenHasFields {
  89. if whitespace(value[z]) {
  90. break
  91. }
  92. } else {
  93. if whitespace(value[z]) || value[z] == ',' {
  94. break
  95. }
  96. }
  97. z++
  98. }
  99. i = z
  100. result := value[k:z]
  101. if result != "" && result[len(result)-1] == ',' {
  102. result = result[:len(result)-1]
  103. }
  104. err = cd.addPair(token, result)
  105. }
  106. } else {
  107. if token != "," {
  108. err = cd.addToken(token)
  109. }
  110. i = j
  111. }
  112. }
  113. return err
  114. }
  115. // DeltaSeconds specifies a non-negative integer, representing
  116. // time in seconds: http://tools.ietf.org/html/rfc7234#section-1.2.1
  117. //
  118. // When set to -1, this means unset.
  119. //
  120. type DeltaSeconds int32
  121. // Parser for delta-seconds, a uint31, more or less:
  122. // http://tools.ietf.org/html/rfc7234#section-1.2.1
  123. func parseDeltaSeconds(v string) (DeltaSeconds, error) {
  124. n, err := strconv.ParseUint(v, 10, 32)
  125. if err != nil {
  126. if numError, ok := err.(*strconv.NumError); ok {
  127. if numError.Err == strconv.ErrRange {
  128. return DeltaSeconds(math.MaxInt32), nil
  129. }
  130. }
  131. return DeltaSeconds(-1), err
  132. } else {
  133. if n > math.MaxInt32 {
  134. return DeltaSeconds(math.MaxInt32), nil
  135. } else {
  136. return DeltaSeconds(n), nil
  137. }
  138. }
  139. }
  140. // Fields present in a header.
  141. type FieldNames map[string]bool
  142. // internal interface for shared methods of RequestCacheDirectives and ResponseCacheDirectives
  143. type cacheDirective interface {
  144. addToken(s string) error
  145. addPair(s string, v string) error
  146. }
  147. // LOW LEVEL API: Repersentation of possible request directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.1
  148. //
  149. // Note: Many fields will be `nil` in practice.
  150. //
  151. type RequestCacheDirectives struct {
  152. // max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.1
  153. //
  154. // The "max-age" request directive indicates that the client is
  155. // unwilling to accept a response whose age is greater than the
  156. // specified number of seconds. Unless the max-stale request directive
  157. // is also present, the client is not willing to accept a stale
  158. // response.
  159. MaxAge DeltaSeconds
  160. // max-stale(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.2
  161. //
  162. // The "max-stale" request directive indicates that the client is
  163. // willing to accept a response that has exceeded its freshness
  164. // lifetime. If max-stale is assigned a value, then the client is
  165. // willing to accept a response that has exceeded its freshness lifetime
  166. // by no more than the specified number of seconds. If no value is
  167. // assigned to max-stale, then the client is willing to accept a stale
  168. // response of any age.
  169. MaxStale DeltaSeconds
  170. // min-fresh(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.3
  171. //
  172. // The "min-fresh" request directive indicates that the client is
  173. // willing to accept a response whose freshness lifetime is no less than
  174. // its current age plus the specified time in seconds. That is, the
  175. // client wants a response that will still be fresh for at least the
  176. // specified number of seconds.
  177. MinFresh DeltaSeconds
  178. // no-cache(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.4
  179. //
  180. // The "no-cache" request directive indicates that a cache MUST NOT use
  181. // a stored response to satisfy the request without successful
  182. // validation on the origin server.
  183. NoCache bool
  184. // no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.5
  185. //
  186. // The "no-store" request directive indicates that a cache MUST NOT
  187. // store any part of either this request or any response to it. This
  188. // directive applies to both private and shared caches.
  189. NoStore bool
  190. // no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.6
  191. //
  192. // The "no-transform" request directive indicates that an intermediary
  193. // (whether or not it implements a cache) MUST NOT transform the
  194. // payload, as defined in Section 5.7.2 of RFC7230.
  195. NoTransform bool
  196. // only-if-cached(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.7
  197. //
  198. // The "only-if-cached" request directive indicates that the client only
  199. // wishes to obtain a stored response.
  200. OnlyIfCached bool
  201. // Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
  202. //
  203. // The Cache-Control header field can be extended through the use of one
  204. // or more cache-extension tokens, each with an optional value. A cache
  205. // MUST ignore unrecognized cache directives.
  206. Extensions []string
  207. }
  208. func (cd *RequestCacheDirectives) addToken(token string) error {
  209. var err error = nil
  210. switch token {
  211. case "max-age":
  212. err = ErrMaxAgeDeltaSeconds
  213. case "max-stale":
  214. err = ErrMaxStaleDeltaSeconds
  215. case "min-fresh":
  216. err = ErrMinFreshDeltaSeconds
  217. case "no-cache":
  218. cd.NoCache = true
  219. case "no-store":
  220. cd.NoStore = true
  221. case "no-transform":
  222. cd.NoTransform = true
  223. case "only-if-cached":
  224. cd.OnlyIfCached = true
  225. default:
  226. cd.Extensions = append(cd.Extensions, token)
  227. }
  228. return err
  229. }
  230. func (cd *RequestCacheDirectives) addPair(token string, v string) error {
  231. var err error = nil
  232. switch token {
  233. case "max-age":
  234. cd.MaxAge, err = parseDeltaSeconds(v)
  235. if err != nil {
  236. err = ErrMaxAgeDeltaSeconds
  237. }
  238. case "max-stale":
  239. cd.MaxStale, err = parseDeltaSeconds(v)
  240. if err != nil {
  241. err = ErrMaxStaleDeltaSeconds
  242. }
  243. case "min-fresh":
  244. cd.MinFresh, err = parseDeltaSeconds(v)
  245. if err != nil {
  246. err = ErrMinFreshDeltaSeconds
  247. }
  248. case "no-cache":
  249. err = ErrNoCacheNoArgs
  250. case "no-store":
  251. err = ErrNoStoreNoArgs
  252. case "no-transform":
  253. err = ErrNoTransformNoArgs
  254. case "only-if-cached":
  255. err = ErrOnlyIfCachedNoArgs
  256. default:
  257. // TODO(pquerna): this sucks, making user re-parse
  258. cd.Extensions = append(cd.Extensions, token+"="+v)
  259. }
  260. return err
  261. }
  262. // LOW LEVEL API: Parses a Cache Control Header from a Request into a set of directives.
  263. func ParseRequestCacheControl(value string) (*RequestCacheDirectives, error) {
  264. cd := &RequestCacheDirectives{
  265. MaxAge: -1,
  266. MaxStale: -1,
  267. MinFresh: -1,
  268. }
  269. err := parse(value, cd)
  270. if err != nil {
  271. return nil, err
  272. }
  273. return cd, nil
  274. }
  275. // LOW LEVEL API: Repersentation of possible response directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.2
  276. //
  277. // Note: Many fields will be `nil` in practice.
  278. //
  279. type ResponseCacheDirectives struct {
  280. // must-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.1
  281. //
  282. // The "must-revalidate" response directive indicates that once it has
  283. // become stale, a cache MUST NOT use the response to satisfy subsequent
  284. // requests without successful validation on the origin server.
  285. MustRevalidate bool
  286. // no-cache(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
  287. //
  288. // The "no-cache" response directive indicates that the response MUST
  289. // NOT be used to satisfy a subsequent request without successful
  290. // validation on the origin server.
  291. //
  292. // If the no-cache response directive specifies one or more field-names,
  293. // then a cache MAY use the response to satisfy a subsequent request,
  294. // subject to any other restrictions on caching. However, any header
  295. // fields in the response that have the field-name(s) listed MUST NOT be
  296. // sent in the response to a subsequent request without successful
  297. // revalidation with the origin server.
  298. NoCache FieldNames
  299. // no-cache(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
  300. //
  301. // While the RFC defines optional field-names on a no-cache directive,
  302. // many applications only want to know if any no-cache directives were
  303. // present at all.
  304. NoCachePresent bool
  305. // no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.3
  306. //
  307. // The "no-store" request directive indicates that a cache MUST NOT
  308. // store any part of either this request or any response to it. This
  309. // directive applies to both private and shared caches.
  310. NoStore bool
  311. // no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.4
  312. //
  313. // The "no-transform" response directive indicates that an intermediary
  314. // (regardless of whether it implements a cache) MUST NOT transform the
  315. // payload, as defined in Section 5.7.2 of RFC7230.
  316. NoTransform bool
  317. // public(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.5
  318. //
  319. // The "public" response directive indicates that any cache MAY store
  320. // the response, even if the response would normally be non-cacheable or
  321. // cacheable only within a private cache.
  322. Public bool
  323. // private(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
  324. //
  325. // The "private" response directive indicates that the response message
  326. // is intended for a single user and MUST NOT be stored by a shared
  327. // cache. A private cache MAY store the response and reuse it for later
  328. // requests, even if the response would normally be non-cacheable.
  329. //
  330. // If the private response directive specifies one or more field-names,
  331. // this requirement is limited to the field-values associated with the
  332. // listed response header fields. That is, a shared cache MUST NOT
  333. // store the specified field-names(s), whereas it MAY store the
  334. // remainder of the response message.
  335. Private FieldNames
  336. // private(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
  337. //
  338. // While the RFC defines optional field-names on a private directive,
  339. // many applications only want to know if any private directives were
  340. // present at all.
  341. PrivatePresent bool
  342. // proxy-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.7
  343. //
  344. // The "proxy-revalidate" response directive has the same meaning as the
  345. // must-revalidate response directive, except that it does not apply to
  346. // private caches.
  347. ProxyRevalidate bool
  348. // max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.8
  349. //
  350. // The "max-age" response directive indicates that the response is to be
  351. // considered stale after its age is greater than the specified number
  352. // of seconds.
  353. MaxAge DeltaSeconds
  354. // s-maxage(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.9
  355. //
  356. // The "s-maxage" response directive indicates that, in shared caches,
  357. // the maximum age specified by this directive overrides the maximum age
  358. // specified by either the max-age directive or the Expires header
  359. // field. The s-maxage directive also implies the semantics of the
  360. // proxy-revalidate response directive.
  361. SMaxAge DeltaSeconds
  362. ////
  363. // Experimental features
  364. // - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Extension_Cache-Control_directives
  365. // - https://www.fastly.com/blog/stale-while-revalidate-stale-if-error-available-today
  366. ////
  367. // immutable(cast-to-bool): experimental feature
  368. Immutable bool
  369. // stale-if-error(delta seconds): experimental feature
  370. StaleIfError DeltaSeconds
  371. // stale-while-revalidate(delta seconds): experimental feature
  372. StaleWhileRevalidate DeltaSeconds
  373. // Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
  374. //
  375. // The Cache-Control header field can be extended through the use of one
  376. // or more cache-extension tokens, each with an optional value. A cache
  377. // MUST ignore unrecognized cache directives.
  378. Extensions []string
  379. }
  380. // LOW LEVEL API: Parses a Cache Control Header from a Response into a set of directives.
  381. func ParseResponseCacheControl(value string) (*ResponseCacheDirectives, error) {
  382. cd := &ResponseCacheDirectives{
  383. MaxAge: -1,
  384. SMaxAge: -1,
  385. // Exerimantal stale timeouts
  386. StaleIfError: -1,
  387. StaleWhileRevalidate: -1,
  388. }
  389. err := parse(value, cd)
  390. if err != nil {
  391. return nil, err
  392. }
  393. return cd, nil
  394. }
  395. func (cd *ResponseCacheDirectives) addToken(token string) error {
  396. var err error = nil
  397. switch token {
  398. case "must-revalidate":
  399. cd.MustRevalidate = true
  400. case "no-cache":
  401. cd.NoCachePresent = true
  402. case "no-store":
  403. cd.NoStore = true
  404. case "no-transform":
  405. cd.NoTransform = true
  406. case "public":
  407. cd.Public = true
  408. case "private":
  409. cd.PrivatePresent = true
  410. case "proxy-revalidate":
  411. cd.ProxyRevalidate = true
  412. case "max-age":
  413. err = ErrMaxAgeDeltaSeconds
  414. case "s-maxage":
  415. err = ErrSMaxAgeDeltaSeconds
  416. // Experimental
  417. case "immutable":
  418. cd.Immutable = true
  419. case "stale-if-error":
  420. err = ErrMaxAgeDeltaSeconds
  421. case "stale-while-revalidate":
  422. err = ErrMaxAgeDeltaSeconds
  423. default:
  424. cd.Extensions = append(cd.Extensions, token)
  425. }
  426. return err
  427. }
  428. func hasFieldNames(token string) bool {
  429. switch token {
  430. case "no-cache":
  431. return true
  432. case "private":
  433. return true
  434. }
  435. return false
  436. }
  437. func (cd *ResponseCacheDirectives) addPair(token string, v string) error {
  438. var err error = nil
  439. switch token {
  440. case "must-revalidate":
  441. err = ErrMustRevalidateNoArgs
  442. case "no-cache":
  443. cd.NoCachePresent = true
  444. tokens := strings.Split(v, ",")
  445. if cd.NoCache == nil {
  446. cd.NoCache = make(FieldNames)
  447. }
  448. for _, t := range tokens {
  449. k := http.CanonicalHeaderKey(textproto.TrimString(t))
  450. cd.NoCache[k] = true
  451. }
  452. case "no-store":
  453. err = ErrNoStoreNoArgs
  454. case "no-transform":
  455. err = ErrNoTransformNoArgs
  456. case "public":
  457. err = ErrPublicNoArgs
  458. case "private":
  459. cd.PrivatePresent = true
  460. tokens := strings.Split(v, ",")
  461. if cd.Private == nil {
  462. cd.Private = make(FieldNames)
  463. }
  464. for _, t := range tokens {
  465. k := http.CanonicalHeaderKey(textproto.TrimString(t))
  466. cd.Private[k] = true
  467. }
  468. case "proxy-revalidate":
  469. err = ErrProxyRevalidateNoArgs
  470. case "max-age":
  471. cd.MaxAge, err = parseDeltaSeconds(v)
  472. case "s-maxage":
  473. cd.SMaxAge, err = parseDeltaSeconds(v)
  474. // Experimental
  475. case "immutable":
  476. err = ErrImmutableNoArgs
  477. case "stale-if-error":
  478. cd.StaleIfError, err = parseDeltaSeconds(v)
  479. case "stale-while-revalidate":
  480. cd.StaleWhileRevalidate, err = parseDeltaSeconds(v)
  481. default:
  482. // TODO(pquerna): this sucks, making user re-parse, and its technically not 'quoted' like the original,
  483. // but this is still easier, just a SplitN on "="
  484. cd.Extensions = append(cd.Extensions, token+"="+v)
  485. }
  486. return err
  487. }