123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- 'use strict';
- const statusCodeCacheableByDefault = new Set([
- 200,
- 203,
- 204,
- 206,
- 300,
- 301,
- 404,
- 405,
- 410,
- 414,
- 501,
- ]);
- const understoodStatuses = new Set([
- 200,
- 203,
- 204,
- 300,
- 301,
- 302,
- 303,
- 307,
- 308,
- 404,
- 405,
- 410,
- 414,
- 501,
- ]);
- const errorStatusCodes = new Set([
- 500,
- 502,
- 503,
- 504,
- ]);
- const hopByHopHeaders = {
- date: true,
- connection: true,
- 'keep-alive': true,
- 'proxy-authenticate': true,
- 'proxy-authorization': true,
- te: true,
- trailer: true,
- 'transfer-encoding': true,
- upgrade: true,
- };
- const excludedFromRevalidationUpdate = {
-
- 'content-length': true,
- 'content-encoding': true,
- 'transfer-encoding': true,
- 'content-range': true,
- };
- function toNumberOrZero(s) {
- const n = parseInt(s, 10);
- return isFinite(n) ? n : 0;
- }
- function isErrorResponse(response) {
-
- if(!response) {
- return true
- }
- return errorStatusCodes.has(response.status);
- }
- function parseCacheControl(header) {
- const cc = {};
- if (!header) return cc;
-
-
- const parts = header.trim().split(/\s*,\s*/);
- for (const part of parts) {
- const [k, v] = part.split(/\s*=\s*/, 2);
- cc[k] = v === undefined ? true : v.replace(/^"|"$/g, '');
- }
- return cc;
- }
- function formatCacheControl(cc) {
- let parts = [];
- for (const k in cc) {
- const v = cc[k];
- parts.push(v === true ? k : k + '=' + v);
- }
- if (!parts.length) {
- return undefined;
- }
- return parts.join(', ');
- }
- module.exports = class CachePolicy {
- constructor(
- req,
- res,
- {
- shared,
- cacheHeuristic,
- immutableMinTimeToLive,
- ignoreCargoCult,
- _fromObject,
- } = {}
- ) {
- if (_fromObject) {
- this._fromObject(_fromObject);
- return;
- }
- if (!res || !res.headers) {
- throw Error('Response headers missing');
- }
- this._assertRequestHasHeaders(req);
- this._responseTime = this.now();
- this._isShared = shared !== false;
- this._cacheHeuristic =
- undefined !== cacheHeuristic ? cacheHeuristic : 0.1;
- this._immutableMinTtl =
- undefined !== immutableMinTimeToLive
- ? immutableMinTimeToLive
- : 24 * 3600 * 1000;
- this._status = 'status' in res ? res.status : 200;
- this._resHeaders = res.headers;
- this._rescc = parseCacheControl(res.headers['cache-control']);
- this._method = 'method' in req ? req.method : 'GET';
- this._url = req.url;
- this._host = req.headers.host;
- this._noAuthorization = !req.headers.authorization;
- this._reqHeaders = res.headers.vary ? req.headers : null;
- this._reqcc = parseCacheControl(req.headers['cache-control']);
-
-
- if (
- ignoreCargoCult &&
- 'pre-check' in this._rescc &&
- 'post-check' in this._rescc
- ) {
- delete this._rescc['pre-check'];
- delete this._rescc['post-check'];
- delete this._rescc['no-cache'];
- delete this._rescc['no-store'];
- delete this._rescc['must-revalidate'];
- this._resHeaders = Object.assign({}, this._resHeaders, {
- 'cache-control': formatCacheControl(this._rescc),
- });
- delete this._resHeaders.expires;
- delete this._resHeaders.pragma;
- }
-
-
- if (
- res.headers['cache-control'] == null &&
- /no-cache/.test(res.headers.pragma)
- ) {
- this._rescc['no-cache'] = true;
- }
- }
- now() {
- return Date.now();
- }
- storable() {
-
- return !!(
- !this._reqcc['no-store'] &&
-
-
- ('GET' === this._method ||
- 'HEAD' === this._method ||
- ('POST' === this._method && this._hasExplicitExpiration())) &&
-
- understoodStatuses.has(this._status) &&
-
- !this._rescc['no-store'] &&
-
- (!this._isShared || !this._rescc.private) &&
-
- (!this._isShared ||
- this._noAuthorization ||
- this._allowsStoringAuthenticated()) &&
-
-
- (this._resHeaders.expires ||
-
-
-
- this._rescc['max-age'] ||
- (this._isShared && this._rescc['s-maxage']) ||
- this._rescc.public ||
-
- statusCodeCacheableByDefault.has(this._status))
- );
- }
- _hasExplicitExpiration() {
-
- return (
- (this._isShared && this._rescc['s-maxage']) ||
- this._rescc['max-age'] ||
- this._resHeaders.expires
- );
- }
- _assertRequestHasHeaders(req) {
- if (!req || !req.headers) {
- throw Error('Request headers missing');
- }
- }
- satisfiesWithoutRevalidation(req) {
- this._assertRequestHasHeaders(req);
-
-
-
- const requestCC = parseCacheControl(req.headers['cache-control']);
- if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
- return false;
- }
- if (requestCC['max-age'] && this.age() > requestCC['max-age']) {
- return false;
- }
- if (
- requestCC['min-fresh'] &&
- this.timeToLive() < 1000 * requestCC['min-fresh']
- ) {
- return false;
- }
-
-
- if (this.stale()) {
- const allowsStale =
- requestCC['max-stale'] &&
- !this._rescc['must-revalidate'] &&
- (true === requestCC['max-stale'] ||
- requestCC['max-stale'] > this.age() - this.maxAge());
- if (!allowsStale) {
- return false;
- }
- }
- return this._requestMatches(req, false);
- }
- _requestMatches(req, allowHeadMethod) {
-
- return (
- (!this._url || this._url === req.url) &&
- this._host === req.headers.host &&
-
- (!req.method ||
- this._method === req.method ||
- (allowHeadMethod && 'HEAD' === req.method)) &&
-
- this._varyMatches(req)
- );
- }
- _allowsStoringAuthenticated() {
-
- return (
- this._rescc['must-revalidate'] ||
- this._rescc.public ||
- this._rescc['s-maxage']
- );
- }
- _varyMatches(req) {
- if (!this._resHeaders.vary) {
- return true;
- }
-
- if (this._resHeaders.vary === '*') {
- return false;
- }
- const fields = this._resHeaders.vary
- .trim()
- .toLowerCase()
- .split(/\s*,\s*/);
- for (const name of fields) {
- if (req.headers[name] !== this._reqHeaders[name]) return false;
- }
- return true;
- }
- _copyWithoutHopByHopHeaders(inHeaders) {
- const headers = {};
- for (const name in inHeaders) {
- if (hopByHopHeaders[name]) continue;
- headers[name] = inHeaders[name];
- }
-
- if (inHeaders.connection) {
- const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
- for (const name of tokens) {
- delete headers[name];
- }
- }
- if (headers.warning) {
- const warnings = headers.warning.split(/,/).filter(warning => {
- return !/^\s*1[0-9][0-9]/.test(warning);
- });
- if (!warnings.length) {
- delete headers.warning;
- } else {
- headers.warning = warnings.join(',').trim();
- }
- }
- return headers;
- }
- responseHeaders() {
- const headers = this._copyWithoutHopByHopHeaders(this._resHeaders);
- const age = this.age();
-
-
- if (
- age > 3600 * 24 &&
- !this._hasExplicitExpiration() &&
- this.maxAge() > 3600 * 24
- ) {
- headers.warning =
- (headers.warning ? `${headers.warning}, ` : '') +
- '113 - "rfc7234 5.5.4"';
- }
- headers.age = `${Math.round(age)}`;
- headers.date = new Date(this.now()).toUTCString();
- return headers;
- }
-
- date() {
- const serverDate = Date.parse(this._resHeaders.date);
- if (isFinite(serverDate)) {
- return serverDate;
- }
- return this._responseTime;
- }
-
- age() {
- let age = this._ageValue();
- const residentTime = (this.now() - this._responseTime) / 1000;
- return age + residentTime;
- }
- _ageValue() {
- return toNumberOrZero(this._resHeaders.age);
- }
-
- maxAge() {
- if (!this.storable() || this._rescc['no-cache']) {
- return 0;
- }
-
-
- if (
- this._isShared &&
- (this._resHeaders['set-cookie'] &&
- !this._rescc.public &&
- !this._rescc.immutable)
- ) {
- return 0;
- }
- if (this._resHeaders.vary === '*') {
- return 0;
- }
- if (this._isShared) {
- if (this._rescc['proxy-revalidate']) {
- return 0;
- }
-
- if (this._rescc['s-maxage']) {
- return toNumberOrZero(this._rescc['s-maxage']);
- }
- }
-
- if (this._rescc['max-age']) {
- return toNumberOrZero(this._rescc['max-age']);
- }
- const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
- const serverDate = this.date();
- if (this._resHeaders.expires) {
- const expires = Date.parse(this._resHeaders.expires);
-
- if (Number.isNaN(expires) || expires < serverDate) {
- return 0;
- }
- return Math.max(defaultMinTtl, (expires - serverDate) / 1000);
- }
- if (this._resHeaders['last-modified']) {
- const lastModified = Date.parse(this._resHeaders['last-modified']);
- if (isFinite(lastModified) && serverDate > lastModified) {
- return Math.max(
- defaultMinTtl,
- ((serverDate - lastModified) / 1000) * this._cacheHeuristic
- );
- }
- }
- return defaultMinTtl;
- }
- timeToLive() {
- const age = this.maxAge() - this.age();
- const staleIfErrorAge = age + toNumberOrZero(this._rescc['stale-if-error']);
- const staleWhileRevalidateAge = age + toNumberOrZero(this._rescc['stale-while-revalidate']);
- return Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000;
- }
- stale() {
- return this.maxAge() <= this.age();
- }
- _useStaleIfError() {
- return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
- }
- useStaleWhileRevalidate() {
- return this.maxAge() + toNumberOrZero(this._rescc['stale-while-revalidate']) > this.age();
- }
- static fromObject(obj) {
- return new this(undefined, undefined, { _fromObject: obj });
- }
- _fromObject(obj) {
- if (this._responseTime) throw Error('Reinitialized');
- if (!obj || obj.v !== 1) throw Error('Invalid serialization');
- this._responseTime = obj.t;
- this._isShared = obj.sh;
- this._cacheHeuristic = obj.ch;
- this._immutableMinTtl =
- obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000;
- this._status = obj.st;
- this._resHeaders = obj.resh;
- this._rescc = obj.rescc;
- this._method = obj.m;
- this._url = obj.u;
- this._host = obj.h;
- this._noAuthorization = obj.a;
- this._reqHeaders = obj.reqh;
- this._reqcc = obj.reqcc;
- }
- toObject() {
- return {
- v: 1,
- t: this._responseTime,
- sh: this._isShared,
- ch: this._cacheHeuristic,
- imm: this._immutableMinTtl,
- st: this._status,
- resh: this._resHeaders,
- rescc: this._rescc,
- m: this._method,
- u: this._url,
- h: this._host,
- a: this._noAuthorization,
- reqh: this._reqHeaders,
- reqcc: this._reqcc,
- };
- }
-
- revalidationHeaders(incomingReq) {
- this._assertRequestHasHeaders(incomingReq);
- const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers);
-
- delete headers['if-range'];
- if (!this._requestMatches(incomingReq, true) || !this.storable()) {
-
-
- delete headers['if-none-match'];
- delete headers['if-modified-since'];
- return headers;
- }
-
- if (this._resHeaders.etag) {
- headers['if-none-match'] = headers['if-none-match']
- ? `${headers['if-none-match']}, ${this._resHeaders.etag}`
- : this._resHeaders.etag;
- }
-
- const forbidsWeakValidators =
- headers['accept-ranges'] ||
- headers['if-match'] ||
- headers['if-unmodified-since'] ||
- (this._method && this._method != 'GET');
-
- if (forbidsWeakValidators) {
- delete headers['if-modified-since'];
- if (headers['if-none-match']) {
- const etags = headers['if-none-match']
- .split(/,/)
- .filter(etag => {
- return !/^\s*W\//.test(etag);
- });
- if (!etags.length) {
- delete headers['if-none-match'];
- } else {
- headers['if-none-match'] = etags.join(',').trim();
- }
- }
- } else if (
- this._resHeaders['last-modified'] &&
- !headers['if-modified-since']
- ) {
- headers['if-modified-since'] = this._resHeaders['last-modified'];
- }
- return headers;
- }
-
- revalidatedPolicy(request, response) {
- this._assertRequestHasHeaders(request);
- if(this._useStaleIfError() && isErrorResponse(response)) {
- return {
- modified: false,
- matches: false,
- policy: this,
- };
- }
- if (!response || !response.headers) {
- throw Error('Response headers missing');
- }
-
-
- let matches = false;
- if (response.status !== undefined && response.status != 304) {
- matches = false;
- } else if (
- response.headers.etag &&
- !/^\s*W\//.test(response.headers.etag)
- ) {
-
-
-
- matches =
- this._resHeaders.etag &&
- this._resHeaders.etag.replace(/^\s*W\//, '') ===
- response.headers.etag;
- } else if (this._resHeaders.etag && response.headers.etag) {
-
-
-
- matches =
- this._resHeaders.etag.replace(/^\s*W\//, '') ===
- response.headers.etag.replace(/^\s*W\//, '');
- } else if (this._resHeaders['last-modified']) {
- matches =
- this._resHeaders['last-modified'] ===
- response.headers['last-modified'];
- } else {
-
-
-
-
- if (
- !this._resHeaders.etag &&
- !this._resHeaders['last-modified'] &&
- !response.headers.etag &&
- !response.headers['last-modified']
- ) {
- matches = true;
- }
- }
- if (!matches) {
- return {
- policy: new this.constructor(request, response),
-
-
-
- modified: response.status != 304,
- matches: false,
- };
- }
-
-
- const headers = {};
- for (const k in this._resHeaders) {
- headers[k] =
- k in response.headers && !excludedFromRevalidationUpdate[k]
- ? response.headers[k]
- : this._resHeaders[k];
- }
- const newResponse = Object.assign({}, response, {
- status: this._status,
- method: this._method,
- headers,
- });
- return {
- policy: new this.constructor(request, newResponse, {
- shared: this._isShared,
- cacheHeuristic: this._cacheHeuristic,
- immutableMinTimeToLive: this._immutableMinTtl,
- }),
- modified: false,
- matches: true,
- };
- }
- };
|