index.js 23 KB

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