policy.js 4.4 KB

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