1const CacheSemantics = require('http-cache-semantics') 2const Negotiator = require('negotiator') 3const ssri = require('ssri') 4 5// options passed to http-cache-semantics constructor 6const policyOptions = { 7 shared: false, 8 ignoreCargoCult: true, 9} 10 11// a fake empty response, used when only testing the 12// request for storability 13const emptyResponse = { status: 200, headers: {} } 14 15// returns a plain object representation of the Request 16const requestObject = (request) => { 17 const _obj = { 18 method: request.method, 19 url: request.url, 20 headers: {}, 21 compress: request.compress, 22 } 23 24 request.headers.forEach((value, key) => { 25 _obj.headers[key] = value 26 }) 27 28 return _obj 29} 30 31// returns a plain object representation of the Response 32const responseObject = (response) => { 33 const _obj = { 34 status: response.status, 35 headers: {}, 36 } 37 38 response.headers.forEach((value, key) => { 39 _obj.headers[key] = value 40 }) 41 42 return _obj 43} 44 45class CachePolicy { 46 constructor ({ entry, request, response, options }) { 47 this.entry = entry 48 this.request = requestObject(request) 49 this.response = responseObject(response) 50 this.options = options 51 this.policy = new CacheSemantics(this.request, this.response, policyOptions) 52 53 if (this.entry) { 54 // if we have an entry, copy the timestamp to the _responseTime 55 // this is necessary because the CacheSemantics constructor forces 56 // the value to Date.now() which means a policy created from a 57 // cache entry is likely to always identify itself as stale 58 this.policy._responseTime = this.entry.metadata.time 59 } 60 } 61 62 // static method to quickly determine if a request alone is storable 63 static storable (request, options) { 64 // no cachePath means no caching 65 if (!options.cachePath) { 66 return false 67 } 68 69 // user explicitly asked not to cache 70 if (options.cache === 'no-store') { 71 return false 72 } 73 74 // we only cache GET and HEAD requests 75 if (!['GET', 'HEAD'].includes(request.method)) { 76 return false 77 } 78 79 // otherwise, let http-cache-semantics make the decision 80 // based on the request's headers 81 const policy = new CacheSemantics(requestObject(request), emptyResponse, policyOptions) 82 return policy.storable() 83 } 84 85 // returns true if the policy satisfies the request 86 satisfies (request) { 87 const _req = requestObject(request) 88 if (this.request.headers.host !== _req.headers.host) { 89 return false 90 } 91 92 if (this.request.compress !== _req.compress) { 93 return false 94 } 95 96 const negotiatorA = new Negotiator(this.request) 97 const negotiatorB = new Negotiator(_req) 98 99 if (JSON.stringify(negotiatorA.mediaTypes()) !== JSON.stringify(negotiatorB.mediaTypes())) { 100 return false 101 } 102 103 if (JSON.stringify(negotiatorA.languages()) !== JSON.stringify(negotiatorB.languages())) { 104 return false 105 } 106 107 if (JSON.stringify(negotiatorA.encodings()) !== JSON.stringify(negotiatorB.encodings())) { 108 return false 109 } 110 111 if (this.options.integrity) { 112 return ssri.parse(this.options.integrity).match(this.entry.integrity) 113 } 114 115 return true 116 } 117 118 // returns true if the request and response allow caching 119 storable () { 120 return this.policy.storable() 121 } 122 123 // NOTE: this is a hack to avoid parsing the cache-control 124 // header ourselves, it returns true if the response's 125 // cache-control contains must-revalidate 126 get mustRevalidate () { 127 return !!this.policy._rescc['must-revalidate'] 128 } 129 130 // returns true if the cached response requires revalidation 131 // for the given request 132 needsRevalidation (request) { 133 const _req = requestObject(request) 134 // force method to GET because we only cache GETs 135 // but can serve a HEAD from a cached GET 136 _req.method = 'GET' 137 return !this.policy.satisfiesWithoutRevalidation(_req) 138 } 139 140 responseHeaders () { 141 return this.policy.responseHeaders() 142 } 143 144 // returns a new object containing the appropriate headers 145 // to send a revalidation request 146 revalidationHeaders (request) { 147 const _req = requestObject(request) 148 return this.policy.revalidationHeaders(_req) 149 } 150 151 // returns true if the request/response was revalidated 152 // successfully. returns false if a new response was received 153 revalidated (request, response) { 154 const _req = requestObject(request) 155 const _res = responseObject(response) 156 const policy = this.policy.revalidatedPolicy(_req, _res) 157 return !policy.modified 158 } 159} 160 161module.exports = CachePolicy 162