source: trip-planner-front/node_modules/http-cache-semantics/index.js@ 8d391a1

Last change on this file since 8d391a1 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

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