Socket
Socket
Sign inDemoInstall

http-cache-semantics

Package Overview
Dependencies
0
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.0.0 to 3.0.0

test/satisfytest.js

102

index.js

@@ -8,3 +8,2 @@ 'use strict';

function parseCacheControl(header) {

@@ -16,12 +15,8 @@ const cc = {};

// the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
const parts = header.split(/\s*,\s*/); // TODO: lame parsing
const parts = header.trim().split(/\s*,\s*/); // TODO: lame parsing
for(const part of parts) {
const [k,v] = part.split(/\s*=\s*/);
const [k,v] = part.split(/\s*=\s*/, 2);
cc[k] = (v === undefined) ? true : v.replace(/^"|"$/g, ''); // TODO: lame unquoting
}
// The s-maxage directive also implies the semantics of the proxy-revalidate response directive.
if ('s-maxage' in cc) {
cc['proxy-revalidate'] = true;
}
return cc;

@@ -40,5 +35,8 @@ }

this._isShared = shared !== false;
this._res = res;
this._status = 'status' in res ? res.status : 200;
this._resHeaders = res.headers;
this._rescc = parseCacheControl(res.headers['cache-control']);
this._req = req;
this._method = 'method' in req ? req.method : 'GET';
this._url = req.url;
this._reqHeaders = req.headers;
this._reqcc = parseCacheControl(req.headers['cache-control']);

@@ -59,4 +57,2 @@

storable() {
const status = (this._res.status === undefined) ? 200 : this._res.status;
// The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.

@@ -66,5 +62,5 @@ return !this._reqcc['no-store'] &&

// The request method is understood by the cache and defined as being cacheable, and
(!this._req.method || 'GET' === this._req.method || 'HEAD' === this._req.method || ('POST' === this._req.method && this._hasExplicitExpiration())) &&
('GET' === this._method || 'HEAD' === this._method || ('POST' === this._method && this._hasExplicitExpiration())) &&
// the response status code is understood by the cache, and
understoodStatuses.includes(status) &&
understoodStatuses.includes(this._status) &&
// the "no-store" cache directive does not appear in request or response header fields, and

@@ -75,7 +71,7 @@ !this._rescc['no-store'] &&

// the Authorization header field does not appear in the request, if the cache is shared,
(!this._isShared || !this._req.headers['authorization'] || this._allowsStoringAuthenticated()) &&
(!this._isShared || !this._reqHeaders.authorization || this._allowsStoringAuthenticated()) &&
// the response either:
(
// contains an Expires header field, or
this._res.headers.expires ||
this._resHeaders.expires ||
// contains a max-age response directive, or

@@ -86,3 +82,3 @@ // contains a s-maxage response directive and the cache is shared, or

// has a status code that is defined as cacheable by default
statusCodeCacheableByDefault.includes(status)
statusCodeCacheableByDefault.includes(this._status)
);

@@ -95,26 +91,52 @@ },

this._rescc['max-age'] ||
this._res.headers.expires;
this._resHeaders.expires;
},
satisfiesWithoutRevalidation(req) {
if (!req || !req.headers) {
throw Error("Request headers missing");
}
// When presented with a request, a cache MUST NOT reuse a stored response, unless:
// the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
// unless the stored response is successfully validated (Section 4.3), and
const requestCC = parseCacheControl(req.headers['cache-control']);
if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
return false;
}
// The presented effective request URI and that of the stored response match, and
return (!this._url || this._url === req.url) &&
(this._reqHeaders.host === req.headers.host) &&
// the request method associated with the stored response allows it to be used for the presented request, and
(!req.method || this._method === req.method) &&
// selecting header fields nominated by the stored response (if any) match those presented, and
this._varyMatches(req) &&
// the stored response is either:
// fresh, or allowed to be served stale
!this.stale() // TODO: allow stale
},
_allowsStoringAuthenticated() {
// following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
return this._rescc['must-revalidate'] || this._rescc['public'] || this._rescc['s-maxage'];
return this._rescc['must-revalidate'] || this._rescc.public || this._rescc['s-maxage'];
},
_varyKeyForRequest(req) {
if (!this._res.headers.vary) return '';
_varyMatches(req) {
if (!this._resHeaders.vary) {
return true;
}
let key = '';
const fields = this._res.headers.vary.toLowerCase().split(/\s*,\s*/);
fields.sort();
// A Vary header field-value of "*" always fails to match
if (this._reqHeaders.vary === '*') {
return false;
}
const fields = this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);
for(const name of fields) {
key += `${name}:${req.headers[name] || '÷'}\n`;
if (req.headers[name] !== this._reqHeaders[name]) return false;
}
return key;
return true;
},
cacheKey() {
return `${this._req.method || 'GET'} ${this._req.url || ''} ${this._varyKeyForRequest(this._req)}`;
},
/**

@@ -125,3 +147,3 @@ * Value of the Date response header or current time if Date was demed invalid

date() {
const dateValue = Date.parse(this._res.headers.date)
const dateValue = Date.parse(this._resHeaders.date)
const maxClockDrift = 8*3600*1000;

@@ -140,4 +162,4 @@ if (Number.isNaN(dateValue) || dateValue < this._responseTime-maxClockDrift || dateValue > this._responseTime+maxClockDrift) {

let age = Math.max(0, (this._responseTime - this.date())/1000);
if (this._res.headers.age) {
let ageValue = parseInt(this._res.headers.age);
if (this._resHeaders.age) {
let ageValue = parseInt(this._resHeaders.age);
if (isFinite(ageValue)) {

@@ -159,7 +181,7 @@ if (ageValue > age) age = ageValue;

// so this implementation requires explicit opt-in via public header
if (this._isShared && (this._res.headers['set-cookie'] && !this._rescc['public'])) {
if (this._isShared && (this._resHeaders['set-cookie'] && !this._rescc.public)) {
return 0;
}
if (this._res.headers.vary === '*') {
if (this._resHeaders.vary === '*') {
return 0;

@@ -169,2 +191,5 @@ }

if (this._isShared) {
if (this._rescc['proxy-revalidate']) {
return 0;
}
// if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.

@@ -182,4 +207,4 @@ if (this._rescc['s-maxage']) {

const dateValue = this.date();
if (this._res.headers['expires']) {
const expires = Date.parse(this._res.headers['expires']);
if (this._resHeaders.expires) {
const expires = Date.parse(this._resHeaders.expires);
// A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").

@@ -192,4 +217,4 @@ if (Number.isNaN(expires) || expires < dateValue) {

if (this._res.headers['last-modified']) {
const lastModified = Date.parse(this._res.headers['last-modified']);
if (this._resHeaders['last-modified']) {
const lastModified = Date.parse(this._resHeaders['last-modified']);
if (isFinite(lastModified) && dateValue > lastModified) {

@@ -208,2 +233,1 @@ return (dateValue - lastModified) * 0.00001; // In absence of other information cache for 1% of item's age

module.exports = CachePolicy;
{
"name": "http-cache-semantics",
"version": "2.0.0",
"version": "3.0.0",
"description": "Parses Cache-Control headers and friends",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -22,5 +22,6 @@ # HTTP cache semantics

const request = {
url: '/',
method: 'GET',
headers: {
'accept': '*/*',
accept: '*/*',
},

@@ -43,2 +44,12 @@ };

### `satisfiesWithoutRevalidation(request)`
If it returns `true`, then the given `request` matches the response this cache policy has been created with, and the existing response can be used without contacting the server.
If it returns `false`, then the response may not be matching at all (e.g. it's different URL or method), or may require to be refreshed first.
### `storable()`
Returns `true` if the response can be stored in a cache. If it's `false` then you MUST NOT store either request or the response.
### `stale()`

@@ -50,8 +61,2 @@

### `cacheKey()`
Returns a string that is a combination of method, URL, and headers selected with `Vary`.
Note that `Vary: *` never matches any request, so matching of cache keys alone is not sufficient to satisfy a request.
## Implemented

@@ -58,0 +63,0 @@

@@ -34,2 +34,19 @@ 'use strict';

it('Proxy cacheable auth is OK', function() {
const cache = new CachePolicy({method:'GET',headers:{'authorization': 'test'}}, {headers:{'cache-control':'max-age=0,s-maxage=12'}});
assert(!cache.stale());
assert(cache.storable());
});
it('Private auth is OK', function() {
const cache = new CachePolicy({method:'GET',headers:{'authorization': 'test'}}, cacheableResponse, {shared:false});
assert(!cache.stale());
assert(cache.storable());
});
it('Revalidated auth is OK', function() {
const cache = new CachePolicy({headers:{'authorization': 'test'}}, {headers:{'cache-control':'max-age=88,must-revalidate'}});
assert(cache.storable());
});
it('Auth prevents caching by default', function() {

@@ -36,0 +53,0 @@ const cache = new CachePolicy({method:'GET',headers:{'authorization': 'test'}}, cacheableResponse);

@@ -20,2 +20,14 @@ 'use strict';

it('weird syntax', function() {
const cache = new CachePolicy(req, {headers:{'cache-control': ',,,,max-age = 456 ,'}});
assert(!cache.stale());
assert.equal(cache.maxAge(), 456);
});
it('quoted syntax', function() {
const cache = new CachePolicy(req, {headers:{'cache-control': ' max-age = "678" '}});
assert(!cache.stale());
assert.equal(cache.maxAge(), 678);
});
it('cache with expires', function() {

@@ -40,2 +52,49 @@ const cache = new CachePolicy(req, {headers:{

it('Ages', function() {
let now = 1000;
class TimeTravellingPolicy extends CachePolicy {
now() {
return now;
}
}
const cache = new TimeTravellingPolicy(req, {headers:{
'cache-control':'max-age=100',
'age': '50',
}});
assert(cache.storable());
assert(!cache.stale());
now += 48*1000;
assert(!cache.stale());
now += 5*1000;
assert(cache.stale());
});
it('Age can make stale', function() {
const cache = new CachePolicy(req, {headers:{
'cache-control':'max-age=100',
'age': '101',
}});
assert(cache.stale());
assert(cache.storable());
});
it('Age not always stale', function() {
const cache = new CachePolicy(req, {headers:{
'cache-control':'max-age=20',
'age': '15',
}});
assert(!cache.stale());
assert(cache.storable());
});
it('Bogus age ignored', function() {
const cache = new CachePolicy(req, {headers:{
'cache-control':'max-age=20',
'age': 'golden',
}});
assert(!cache.stale());
assert(cache.storable());
});
it('cache old files', function() {

@@ -42,0 +101,0 @@ const cache = new CachePolicy(req, {headers:{

@@ -8,69 +8,69 @@ 'use strict';

it('Basic', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'weather'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'WEATHER'}});
const policy = new CachePolicy({headers:{'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'weather'}});
assert.equal(cache1.cacheKey(), cache1.cacheKey());
assert.equal(cache2.cacheKey(), cache2.cacheKey());
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'bad'}}));
});
it("* doesn't match other", function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'vary':'*'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'vary':'weather'}});
it("* doesn't match", function() {
const policy = new CachePolicy({headers:{'weather': 'ok'}}, {headers:{'cache-control':'max-age=5','vary':'*'}});
assert.equal(cache2.cacheKey(), cache2.cacheKey());
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'ok'}}));
});
it("* is stale", function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'*'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'weather'}});
const policy1 = new CachePolicy({headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'*'}});
const policy2 = new CachePolicy({headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'weather'}});
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(cache1.stale());
assert(!cache2.stale());
assert(policy1.stale());
assert(!policy2.stale());
});
it('Values are case-sensitive', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'BAD'}}, {headers:{'vary':'weather'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'weather'}});
const policy = new CachePolicy({headers:{'weather': 'BAD'}}, {headers:{'cache-control':'max-age=5','vary':'Weather'}});
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'BAD'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'bad'}}));
});
it('Irrelevant headers ignored', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'moon-phase'}});
const policy = new CachePolicy({headers:{'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'moon-phase'}});
assert.equal(cache1.cacheKey(), cache1.cacheKey());
assert.equal(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'bad'}}));
assert(policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'moon-phase': 'full'}}));
});
it('Absence is meaningful', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'sunshine'}});
const policy = new CachePolicy({headers:{'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'moon-phase, weather'}});
assert.equal(cache2.cacheKey(), cache2.cacheKey());
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice', 'moon-phase': ''}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{}}));
});
it('All values must match', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'vary':'weather, sun'}});
const cache2 = new CachePolicy({method:'GET',headers:{'sun': 'shining', 'weather': 'bad'}}, {headers:{'vary':'weather, sun'}});
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
const policy = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'weather, sun'}});
assert(policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'bad'}}));
});
it('Whitespace is OK', function() {
const policy = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':' weather , sun '}});
assert(policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining'}}));
});
it('Order is irrelevant', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase, SUNSHINE'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'sunshine, moon-phase'}});
assert.equal(cache1.cacheKey(), cache2.cacheKey());
const policy1 = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'weather, sun'}});
const policy2 = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'sun, weather'}});
const cache3 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase, weather'}});
const cache4 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'weather, moon-phase'}});
assert.equal(cache3.cacheKey(), cache4.cacheKey());
assert.notEqual(cache1.cacheKey(), cache3.cacheKey());
assert.notEqual(cache2.cacheKey(), cache4.cacheKey());
assert(policy1.satisfiesWithoutRevalidation({headers:{'weather': 'nice', 'sun': 'shining'}}));
assert(policy1.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
assert(policy2.satisfiesWithoutRevalidation({headers:{'weather': 'nice', 'sun': 'shining'}}));
assert(policy2.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc