index.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. 'use strict';
  2. // rfc7231 6.1
  3. const statusCodeCacheableByDefault = new Set([
  4. 200,
  5. 203,
  6. 204,
  7. 206,
  8. 300,
  9. 301,
  10. 308,
  11. 404,
  12. 405,
  13. 410,
  14. 414,
  15. 501,
  16. ]);
  17. // This implementation does not understand partial responses (206)
  18. const understoodStatuses = new Set([
  19. 200,
  20. 203,
  21. 204,
  22. 300,
  23. 301,
  24. 302,
  25. 303,
  26. 307,
  27. 308,
  28. 404,
  29. 405,
  30. 410,
  31. 414,
  32. 501,
  33. ]);
  34. const errorStatusCodes = new Set([
  35. 500,
  36. 502,
  37. 503,
  38. 504,
  39. ]);
  40. const hopByHopHeaders = {
  41. date: true, // included, because we add Age update Date
  42. connection: true,
  43. 'keep-alive': true,
  44. 'proxy-authenticate': true,
  45. 'proxy-authorization': true,
  46. te: true,
  47. trailer: true,
  48. 'transfer-encoding': true,
  49. upgrade: true,
  50. };
  51. const excludedFromRevalidationUpdate = {
  52. // Since the old body is reused, it doesn't make sense to change properties of the body
  53. 'content-length': true,
  54. 'content-encoding': true,
  55. 'transfer-encoding': true,
  56. 'content-range': true,
  57. };
  58. function toNumberOrZero(s) {
  59. const n = parseInt(s, 10);
  60. return isFinite(n) ? n : 0;
  61. }
  62. // RFC 5861
  63. function isErrorResponse(response) {
  64. // consider undefined response as faulty
  65. if(!response) {
  66. return true
  67. }
  68. return errorStatusCodes.has(response.status);
  69. }
  70. function parseCacheControl(header) {
  71. const cc = {};
  72. if (!header) return cc;
  73. // TODO: When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives),
  74. // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
  75. const parts = header.trim().split(/,/);
  76. for (const part of parts) {
  77. const [k, v] = part.split(/=/, 2);
  78. cc[k.trim()] = v === undefined ? true : v.trim().replace(/^"|"$/g, '');
  79. }
  80. return cc;
  81. }
  82. function formatCacheControl(cc) {
  83. let parts = [];
  84. for (const k in cc) {
  85. const v = cc[k];
  86. parts.push(v === true ? k : k + '=' + v);
  87. }
  88. if (!parts.length) {
  89. return undefined;
  90. }
  91. return parts.join(', ');
  92. }
  93. module.exports = class CachePolicy {
  94. constructor(
  95. req,
  96. res,
  97. {
  98. shared,
  99. cacheHeuristic,
  100. immutableMinTimeToLive,
  101. ignoreCargoCult,
  102. _fromObject,
  103. } = {}
  104. ) {
  105. if (_fromObject) {
  106. this._fromObject(_fromObject);
  107. return;
  108. }
  109. if (!res || !res.headers) {
  110. throw Error('Response headers missing');
  111. }
  112. this._assertRequestHasHeaders(req);
  113. this._responseTime = this.now();
  114. this._isShared = shared !== false;
  115. this._cacheHeuristic =
  116. undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
  117. this._immutableMinTtl =
  118. undefined !== immutableMinTimeToLive
  119. ? immutableMinTimeToLive
  120. : 24 * 3600 * 1000;
  121. this._status = 'status' in res ? res.status : 200;
  122. this._resHeaders = res.headers;
  123. this._rescc = parseCacheControl(res.headers['cache-control']);
  124. this._method = 'method' in req ? req.method : 'GET';
  125. this._url = req.url;
  126. this._host = req.headers.host;
  127. this._noAuthorization = !req.headers.authorization;
  128. this._reqHeaders = res.headers.vary ? req.headers : null; // Don't keep all request headers if they won't be used
  129. this._reqcc = parseCacheControl(req.headers['cache-control']);
  130. // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching,
  131. // so there's no point stricly adhering to the blindly copy&pasted directives.
  132. if (
  133. ignoreCargoCult &&
  134. 'pre-check' in this._rescc &&
  135. 'post-check' in this._rescc
  136. ) {
  137. delete this._rescc['pre-check'];
  138. delete this._rescc['post-check'];
  139. delete this._rescc['no-cache'];
  140. delete this._rescc['no-store'];
  141. delete this._rescc['must-revalidate'];
  142. this._resHeaders = Object.assign({}, this._resHeaders, {
  143. 'cache-control': formatCacheControl(this._rescc),
  144. });
  145. delete this._resHeaders.expires;
  146. delete this._resHeaders.pragma;
  147. }
  148. // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive
  149. // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
  150. if (
  151. res.headers['cache-control'] == null &&
  152. /no-cache/.test(res.headers.pragma)
  153. ) {
  154. this._rescc['no-cache'] = true;
  155. }
  156. }
  157. now() {
  158. return Date.now();
  159. }
  160. storable() {
  161. // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.
  162. return !!(
  163. !this._reqcc['no-store'] &&
  164. // A cache MUST NOT store a response to any request, unless:
  165. // The request method is understood by the cache and defined as being cacheable, and
  166. ('GET' === this._method ||
  167. 'HEAD' === this._method ||
  168. ('POST' === this._method && this._hasExplicitExpiration())) &&
  169. // the response status code is understood by the cache, and
  170. understoodStatuses.has(this._status) &&
  171. // the "no-store" cache directive does not appear in request or response header fields, and
  172. !this._rescc['no-store'] &&
  173. // the "private" response directive does not appear in the response, if the cache is shared, and
  174. (!this._isShared || !this._rescc.private) &&
  175. // the Authorization header field does not appear in the request, if the cache is shared,
  176. (!this._isShared ||
  177. this._noAuthorization ||
  178. this._allowsStoringAuthenticated()) &&
  179. // the response either:
  180. // contains an Expires header field, or
  181. (this._resHeaders.expires ||
  182. // contains a max-age response directive, or
  183. // contains a s-maxage response directive and the cache is shared, or
  184. // contains a public response directive.
  185. this._rescc['max-age'] ||
  186. (this._isShared && this._rescc['s-maxage']) ||
  187. this._rescc.public ||
  188. // has a status code that is defined as cacheable by default
  189. statusCodeCacheableByDefault.has(this._status))
  190. );
  191. }
  192. _hasExplicitExpiration() {
  193. // 4.2.1 Calculating Freshness Lifetime
  194. return (
  195. (this._isShared && this._rescc['s-maxage']) ||
  196. this._rescc['max-age'] ||
  197. this._resHeaders.expires
  198. );
  199. }
  200. _assertRequestHasHeaders(req) {
  201. if (!req || !req.headers) {
  202. throw Error('Request headers missing');
  203. }
  204. }
  205. satisfiesWithoutRevalidation(req) {
  206. this._assertRequestHasHeaders(req);
  207. // When presented with a request, a cache MUST NOT reuse a stored response, unless:
  208. // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
  209. // unless the stored response is successfully validated (Section 4.3), and
  210. const requestCC = parseCacheControl(req.headers['cache-control']);
  211. if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
  212. return false;
  213. }
  214. if (requestCC['max-age'] && this.age() > requestCC['max-age']) {
  215. return false;
  216. }
  217. if (
  218. requestCC['min-fresh'] &&
  219. this.timeToLive() < 1000 * requestCC['min-fresh']
  220. ) {
  221. return false;
  222. }
  223. // the stored response is either:
  224. // fresh, or allowed to be served stale
  225. if (this.stale()) {
  226. const allowsStale =
  227. requestCC['max-stale'] &&
  228. !this._rescc['must-revalidate'] &&
  229. (true === requestCC['max-stale'] ||
  230. requestCC['max-stale'] > this.age() - this.maxAge());
  231. if (!allowsStale) {
  232. return false;
  233. }
  234. }
  235. return this._requestMatches(req, false);
  236. }
  237. _requestMatches(req, allowHeadMethod) {
  238. // The presented effective request URI and that of the stored response match, and
  239. return (
  240. (!this._url || this._url === req.url) &&
  241. this._host === req.headers.host &&
  242. // the request method associated with the stored response allows it to be used for the presented request, and
  243. (!req.method ||
  244. this._method === req.method ||
  245. (allowHeadMethod && 'HEAD' === req.method)) &&
  246. // selecting header fields nominated by the stored response (if any) match those presented, and
  247. this._varyMatches(req)
  248. );
  249. }
  250. _allowsStoringAuthenticated() {
  251. // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
  252. return (
  253. this._rescc['must-revalidate'] ||
  254. this._rescc.public ||
  255. this._rescc['s-maxage']
  256. );
  257. }
  258. _varyMatches(req) {
  259. if (!this._resHeaders.vary) {
  260. return true;
  261. }
  262. // A Vary header field-value of "*" always fails to match
  263. if (this._resHeaders.vary === '*') {
  264. return false;
  265. }
  266. const fields = this._resHeaders.vary
  267. .trim()
  268. .toLowerCase()
  269. .split(/\s*,\s*/);
  270. for (const name of fields) {
  271. if (req.headers[name] !== this._reqHeaders[name]) return false;
  272. }
  273. return true;
  274. }
  275. _copyWithoutHopByHopHeaders(inHeaders) {
  276. const headers = {};
  277. for (const name in inHeaders) {
  278. if (hopByHopHeaders[name]) continue;
  279. headers[name] = inHeaders[name];
  280. }
  281. // 9.1. Connection
  282. if (inHeaders.connection) {
  283. const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
  284. for (const name of tokens) {
  285. delete headers[name];
  286. }
  287. }
  288. if (headers.warning) {
  289. const warnings = headers.warning.split(/,/).filter(warning => {
  290. return !/^\s*1[0-9][0-9]/.test(warning);
  291. });
  292. if (!warnings.length) {
  293. delete headers.warning;
  294. } else {
  295. headers.warning = warnings.join(',').trim();
  296. }
  297. }
  298. return headers;
  299. }
  300. responseHeaders() {
  301. const headers = this._copyWithoutHopByHopHeaders(this._resHeaders);
  302. const age = this.age();
  303. // A cache SHOULD generate 113 warning if it heuristically chose a freshness
  304. // lifetime greater than 24 hours and the response's age is greater than 24 hours.
  305. if (
  306. age > 3600 * 24 &&
  307. !this._hasExplicitExpiration() &&
  308. this.maxAge() > 3600 * 24
  309. ) {
  310. headers.warning =
  311. (headers.warning ? `${headers.warning}, ` : '') +
  312. '113 - "rfc7234 5.5.4"';
  313. }
  314. headers.age = `${Math.round(age)}`;
  315. headers.date = new Date(this.now()).toUTCString();
  316. return headers;
  317. }
  318. /**
  319. * Value of the Date response header or current time if Date was invalid
  320. * @return timestamp
  321. */
  322. date() {
  323. const serverDate = Date.parse(this._resHeaders.date);
  324. if (isFinite(serverDate)) {
  325. return serverDate;
  326. }
  327. return this._responseTime;
  328. }
  329. /**
  330. * Value of the Age header, in seconds, updated for the current time.
  331. * May be fractional.
  332. *
  333. * @return Number
  334. */
  335. age() {
  336. let age = this._ageValue();
  337. const residentTime = (this.now() - this._responseTime) / 1000;
  338. return age + residentTime;
  339. }
  340. _ageValue() {
  341. return toNumberOrZero(this._resHeaders.age);
  342. }
  343. /**
  344. * Value of applicable max-age (or heuristic equivalent) in seconds. This counts since response's `Date`.
  345. *
  346. * For an up-to-date value, see `timeToLive()`.
  347. *
  348. * @return Number
  349. */
  350. maxAge() {
  351. if (!this.storable() || this._rescc['no-cache']) {
  352. return 0;
  353. }
  354. // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default
  355. // so this implementation requires explicit opt-in via public header
  356. if (
  357. this._isShared &&
  358. (this._resHeaders['set-cookie'] &&
  359. !this._rescc.public &&
  360. !this._rescc.immutable)
  361. ) {
  362. return 0;
  363. }
  364. if (this._resHeaders.vary === '*') {
  365. return 0;
  366. }
  367. if (this._isShared) {
  368. if (this._rescc['proxy-revalidate']) {
  369. return 0;
  370. }
  371. // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
  372. if (this._rescc['s-maxage']) {
  373. return toNumberOrZero(this._rescc['s-maxage']);
  374. }
  375. }
  376. // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
  377. if (this._rescc['max-age']) {
  378. return toNumberOrZero(this._rescc['max-age']);
  379. }
  380. const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
  381. const serverDate = this.date();
  382. if (this._resHeaders.expires) {
  383. const expires = Date.parse(this._resHeaders.expires);
  384. // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
  385. if (Number.isNaN(expires) || expires < serverDate) {
  386. return 0;
  387. }
  388. return Math.max(defaultMinTtl, (expires - serverDate) / 1000);
  389. }
  390. if (this._resHeaders['last-modified']) {
  391. const lastModified = Date.parse(this._resHeaders['last-modified']);
  392. if (isFinite(lastModified) && serverDate > lastModified) {
  393. return Math.max(
  394. defaultMinTtl,
  395. ((serverDate - lastModified) / 1000) * this._cacheHeuristic
  396. );
  397. }
  398. }
  399. return defaultMinTtl;
  400. }
  401. timeToLive() {
  402. const age = this.maxAge() - this.age();
  403. const staleIfErrorAge = age + toNumberOrZero(this._rescc['stale-if-error']);
  404. const staleWhileRevalidateAge = age + toNumberOrZero(this._rescc['stale-while-revalidate']);
  405. return Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000;
  406. }
  407. stale() {
  408. return this.maxAge() <= this.age();
  409. }
  410. _useStaleIfError() {
  411. return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
  412. }
  413. useStaleWhileRevalidate() {
  414. return this.maxAge() + toNumberOrZero(this._rescc['stale-while-revalidate']) > this.age();
  415. }
  416. static fromObject(obj) {
  417. return new this(undefined, undefined, { _fromObject: obj });
  418. }
  419. _fromObject(obj) {
  420. if (this._responseTime) throw Error('Reinitialized');
  421. if (!obj || obj.v !== 1) throw Error('Invalid serialization');
  422. this._responseTime = obj.t;
  423. this._isShared = obj.sh;
  424. this._cacheHeuristic = obj.ch;
  425. this._immutableMinTtl =
  426. obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000;
  427. this._status = obj.st;
  428. this._resHeaders = obj.resh;
  429. this._rescc = obj.rescc;
  430. this._method = obj.m;
  431. this._url = obj.u;
  432. this._host = obj.h;
  433. this._noAuthorization = obj.a;
  434. this._reqHeaders = obj.reqh;
  435. this._reqcc = obj.reqcc;
  436. }
  437. toObject() {
  438. return {
  439. v: 1,
  440. t: this._responseTime,
  441. sh: this._isShared,
  442. ch: this._cacheHeuristic,
  443. imm: this._immutableMinTtl,
  444. st: this._status,
  445. resh: this._resHeaders,
  446. rescc: this._rescc,
  447. m: this._method,
  448. u: this._url,
  449. h: this._host,
  450. a: this._noAuthorization,
  451. reqh: this._reqHeaders,
  452. reqcc: this._reqcc,
  453. };
  454. }
  455. /**
  456. * Headers for sending to the origin server to revalidate stale response.
  457. * Allows server to return 304 to allow reuse of the previous response.
  458. *
  459. * Hop by hop headers are always stripped.
  460. * Revalidation headers may be added or removed, depending on request.
  461. */
  462. revalidationHeaders(incomingReq) {
  463. this._assertRequestHasHeaders(incomingReq);
  464. const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers);
  465. // This implementation does not understand range requests
  466. delete headers['if-range'];
  467. if (!this._requestMatches(incomingReq, true) || !this.storable()) {
  468. // revalidation allowed via HEAD
  469. // not for the same resource, or wasn't allowed to be cached anyway
  470. delete headers['if-none-match'];
  471. delete headers['if-modified-since'];
  472. return headers;
  473. }
  474. /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */
  475. if (this._resHeaders.etag) {
  476. headers['if-none-match'] = headers['if-none-match']
  477. ? `${headers['if-none-match']}, ${this._resHeaders.etag}`
  478. : this._resHeaders.etag;
  479. }
  480. // Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request.
  481. const forbidsWeakValidators =
  482. headers['accept-ranges'] ||
  483. headers['if-match'] ||
  484. headers['if-unmodified-since'] ||
  485. (this._method && this._method != 'GET');
  486. /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server.
  487. Note: This implementation does not understand partial responses (206) */
  488. if (forbidsWeakValidators) {
  489. delete headers['if-modified-since'];
  490. if (headers['if-none-match']) {
  491. const etags = headers['if-none-match']
  492. .split(/,/)
  493. .filter(etag => {
  494. return !/^\s*W\//.test(etag);
  495. });
  496. if (!etags.length) {
  497. delete headers['if-none-match'];
  498. } else {
  499. headers['if-none-match'] = etags.join(',').trim();
  500. }
  501. }
  502. } else if (
  503. this._resHeaders['last-modified'] &&
  504. !headers['if-modified-since']
  505. ) {
  506. headers['if-modified-since'] = this._resHeaders['last-modified'];
  507. }
  508. return headers;
  509. }
  510. /**
  511. * Creates new CachePolicy with information combined from the previews response,
  512. * and the new revalidation response.
  513. *
  514. * Returns {policy, modified} where modified is a boolean indicating
  515. * whether the response body has been modified, and old cached body can't be used.
  516. *
  517. * @return {Object} {policy: CachePolicy, modified: Boolean}
  518. */
  519. revalidatedPolicy(request, response) {
  520. this._assertRequestHasHeaders(request);
  521. if(this._useStaleIfError() && isErrorResponse(response)) { // I consider the revalidation request unsuccessful
  522. return {
  523. modified: false,
  524. matches: false,
  525. policy: this,
  526. };
  527. }
  528. if (!response || !response.headers) {
  529. throw Error('Response headers missing');
  530. }
  531. // These aren't going to be supported exactly, since one CachePolicy object
  532. // doesn't know about all the other cached objects.
  533. let matches = false;
  534. if (response.status !== undefined && response.status != 304) {
  535. matches = false;
  536. } else if (
  537. response.headers.etag &&
  538. !/^\s*W\//.test(response.headers.etag)
  539. ) {
  540. // "All of the stored responses with the same strong validator are selected.
  541. // If none of the stored responses contain the same strong validator,
  542. // then the cache MUST NOT use the new response to update any stored responses."
  543. matches =
  544. this._resHeaders.etag &&
  545. this._resHeaders.etag.replace(/^\s*W\//, '') ===
  546. response.headers.etag;
  547. } else if (this._resHeaders.etag && response.headers.etag) {
  548. // "If the new response contains a weak validator and that validator corresponds
  549. // to one of the cache's stored responses,
  550. // then the most recent of those matching stored responses is selected for update."
  551. matches =
  552. this._resHeaders.etag.replace(/^\s*W\//, '') ===
  553. response.headers.etag.replace(/^\s*W\//, '');
  554. } else if (this._resHeaders['last-modified']) {
  555. matches =
  556. this._resHeaders['last-modified'] ===
  557. response.headers['last-modified'];
  558. } else {
  559. // If the new response does not include any form of validator (such as in the case where
  560. // a client generates an If-Modified-Since request from a source other than the Last-Modified
  561. // response header field), and there is only one stored response, and that stored response also
  562. // lacks a validator, then that stored response is selected for update.
  563. if (
  564. !this._resHeaders.etag &&
  565. !this._resHeaders['last-modified'] &&
  566. !response.headers.etag &&
  567. !response.headers['last-modified']
  568. ) {
  569. matches = true;
  570. }
  571. }
  572. if (!matches) {
  573. return {
  574. policy: new this.constructor(request, response),
  575. // Client receiving 304 without body, even if it's invalid/mismatched has no option
  576. // but to reuse a cached body. We don't have a good way to tell clients to do
  577. // error recovery in such case.
  578. modified: response.status != 304,
  579. matches: false,
  580. };
  581. }
  582. // use other header fields provided in the 304 (Not Modified) response to replace all instances
  583. // of the corresponding header fields in the stored response.
  584. const headers = {};
  585. for (const k in this._resHeaders) {
  586. headers[k] =
  587. k in response.headers && !excludedFromRevalidationUpdate[k]
  588. ? response.headers[k]
  589. : this._resHeaders[k];
  590. }
  591. const newResponse = Object.assign({}, response, {
  592. status: this._status,
  593. method: this._method,
  594. headers,
  595. });
  596. return {
  597. policy: new this.constructor(request, newResponse, {
  598. shared: this._isShared,
  599. cacheHeuristic: this._cacheHeuristic,
  600. immutableMinTimeToLive: this._immutableMinTtl,
  601. }),
  602. modified: false,
  603. matches: true,
  604. };
  605. }
  606. };