Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

512 lines
21 KiB

  1. 'use strict';
  2. // rfc7231 6.1
  3. const statusCodeCacheableByDefault = [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501];
  4. // This implementation does not understand partial responses (206)
  5. const understoodStatuses = [200, 203, 204, 300, 301, 302, 303, 307, 308, 404, 405, 410, 414, 501];
  6. const hopByHopHeaders = {
  7. 'date': true, // included, because we add Age update Date
  8. 'connection':true, 'keep-alive':true, 'proxy-authenticate':true, 'proxy-authorization':true, 'te':true, 'trailer':true, 'transfer-encoding':true, 'upgrade':true
  9. };
  10. const excludedFromRevalidationUpdate = {
  11. // Since the old body is reused, it doesn't make sense to change properties of the body
  12. 'content-length': true, 'content-encoding': true, 'transfer-encoding': true,
  13. 'content-range': true,
  14. };
  15. function parseCacheControl(header) {
  16. const cc = {};
  17. if (!header) return cc;
  18. // 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),
  19. // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
  20. const parts = header.trim().split(/\s*,\s*/); // TODO: lame parsing
  21. for(const part of parts) {
  22. const [k,v] = part.split(/\s*=\s*/, 2);
  23. cc[k] = (v === undefined) ? true : v.replace(/^"|"$/g, ''); // TODO: lame unquoting
  24. }
  25. return cc;
  26. }
  27. function formatCacheControl(cc) {
  28. let parts = [];
  29. for(const k in cc) {
  30. const v = cc[k];
  31. parts.push(v === true ? k : k + '=' + v);
  32. }
  33. if (!parts.length) {
  34. return undefined;
  35. }
  36. return parts.join(', ');
  37. }
  38. module.exports = class CachePolicy {
  39. constructor(req, res, {shared, cacheHeuristic, immutableMinTimeToLive, ignoreCargoCult, trustServerDate, _fromObject} = {}) {
  40. if (_fromObject) {
  41. this._fromObject(_fromObject);
  42. return;
  43. }
  44. if (!res || !res.headers) {
  45. throw Error("Response headers missing");
  46. }
  47. this._assertRequestHasHeaders(req);
  48. this._responseTime = this.now();
  49. this._isShared = shared !== false;
  50. this._trustServerDate = undefined !== trustServerDate ? trustServerDate : true;
  51. this._cacheHeuristic = undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
  52. this._immutableMinTtl = undefined !== immutableMinTimeToLive ? immutableMinTimeToLive : 24*3600*1000;
  53. this._status = 'status' in res ? res.status : 200;
  54. this._resHeaders = res.headers;
  55. this._rescc = parseCacheControl(res.headers['cache-control']);
  56. this._method = 'method' in req ? req.method : 'GET';
  57. this._url = req.url;
  58. this._host = req.headers.host;
  59. this._noAuthorization = !req.headers.authorization;
  60. this._reqHeaders = res.headers.vary ? req.headers : null; // Don't keep all request headers if they won't be used
  61. this._reqcc = parseCacheControl(req.headers['cache-control']);
  62. // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching,
  63. // so there's no point stricly adhering to the blindly copy&pasted directives.
  64. if (ignoreCargoCult && "pre-check" in this._rescc && "post-check" in this._rescc) {
  65. delete this._rescc['pre-check'];
  66. delete this._rescc['post-check'];
  67. delete this._rescc['no-cache'];
  68. delete this._rescc['no-store'];
  69. delete this._rescc['must-revalidate'];
  70. this._resHeaders = Object.assign({}, this._resHeaders, {'cache-control': formatCacheControl(this._rescc)});
  71. delete this._resHeaders.expires;
  72. delete this._resHeaders.pragma;
  73. }
  74. // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive
  75. // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
  76. if (!res.headers['cache-control'] && /no-cache/.test(res.headers.pragma)) {
  77. this._rescc['no-cache'] = true;
  78. }
  79. }
  80. now() {
  81. return Date.now();
  82. }
  83. storable() {
  84. // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.
  85. return !!(!this._reqcc['no-store'] &&
  86. // A cache MUST NOT store a response to any request, unless:
  87. // The request method is understood by the cache and defined as being cacheable, and
  88. ('GET' === this._method || 'HEAD' === this._method || ('POST' === this._method && this._hasExplicitExpiration())) &&
  89. // the response status code is understood by the cache, and
  90. understoodStatuses.indexOf(this._status) !== -1 &&
  91. // the "no-store" cache directive does not appear in request or response header fields, and
  92. !this._rescc['no-store'] &&
  93. // the "private" response directive does not appear in the response, if the cache is shared, and
  94. (!this._isShared || !this._rescc.private) &&
  95. // the Authorization header field does not appear in the request, if the cache is shared,
  96. (!this._isShared || this._noAuthorization || this._allowsStoringAuthenticated()) &&
  97. // the response either:
  98. (
  99. // contains an Expires header field, or
  100. this._resHeaders.expires ||
  101. // contains a max-age response directive, or
  102. // contains a s-maxage response directive and the cache is shared, or
  103. // contains a public response directive.
  104. this._rescc.public || this._rescc['max-age'] || this._rescc['s-maxage'] ||
  105. // has a status code that is defined as cacheable by default
  106. statusCodeCacheableByDefault.indexOf(this._status) !== -1
  107. ));
  108. }
  109. _hasExplicitExpiration() {
  110. // 4.2.1 Calculating Freshness Lifetime
  111. return (this._isShared && this._rescc['s-maxage']) ||
  112. this._rescc['max-age'] ||
  113. this._resHeaders.expires;
  114. }
  115. _assertRequestHasHeaders(req) {
  116. if (!req || !req.headers) {
  117. throw Error("Request headers missing");
  118. }
  119. }
  120. satisfiesWithoutRevalidation(req) {
  121. this._assertRequestHasHeaders(req);
  122. // When presented with a request, a cache MUST NOT reuse a stored response, unless:
  123. // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
  124. // unless the stored response is successfully validated (Section 4.3), and
  125. const requestCC = parseCacheControl(req.headers['cache-control']);
  126. if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
  127. return false;
  128. }
  129. if (requestCC['max-age'] && this.age() > requestCC['max-age']) {
  130. return false;
  131. }
  132. if (requestCC['min-fresh'] && this.timeToLive() < 1000*requestCC['min-fresh']) {
  133. return false;
  134. }
  135. // the stored response is either:
  136. // fresh, or allowed to be served stale
  137. if (this.stale()) {
  138. const allowsStale = requestCC['max-stale'] && !this._rescc['must-revalidate'] && (true === requestCC['max-stale'] || requestCC['max-stale'] > this.age() - this.maxAge());
  139. if (!allowsStale) {
  140. return false;
  141. }
  142. }
  143. return this._requestMatches(req, false);
  144. }
  145. _requestMatches(req, allowHeadMethod) {
  146. // The presented effective request URI and that of the stored response match, and
  147. return (!this._url || this._url === req.url) &&
  148. (this._host === req.headers.host) &&
  149. // the request method associated with the stored response allows it to be used for the presented request, and
  150. (!req.method || this._method === req.method || (allowHeadMethod && 'HEAD' === req.method)) &&
  151. // selecting header fields nominated by the stored response (if any) match those presented, and
  152. this._varyMatches(req);
  153. }
  154. _allowsStoringAuthenticated() {
  155. // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
  156. return this._rescc['must-revalidate'] || this._rescc.public || this._rescc['s-maxage'];
  157. }
  158. _varyMatches(req) {
  159. if (!this._resHeaders.vary) {
  160. return true;
  161. }
  162. // A Vary header field-value of "*" always fails to match
  163. if (this._resHeaders.vary === '*') {
  164. return false;
  165. }
  166. const fields = this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);
  167. for(const name of fields) {
  168. if (req.headers[name] !== this._reqHeaders[name]) return false;
  169. }
  170. return true;
  171. }
  172. _copyWithoutHopByHopHeaders(inHeaders) {
  173. const headers = {};
  174. for(const name in inHeaders) {
  175. if (hopByHopHeaders[name]) continue;
  176. headers[name] = inHeaders[name];
  177. }
  178. // 9.1. Connection
  179. if (inHeaders.connection) {
  180. const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
  181. for(const name of tokens) {
  182. delete headers[name];
  183. }
  184. }
  185. if (headers.warning) {
  186. const warnings = headers.warning.split(/,/).filter(warning => {
  187. return !/^\s*1[0-9][0-9]/.test(warning);
  188. });
  189. if (!warnings.length) {
  190. delete headers.warning;
  191. } else {
  192. headers.warning = warnings.join(',').trim();
  193. }
  194. }
  195. return headers;
  196. }
  197. responseHeaders() {
  198. const headers = this._copyWithoutHopByHopHeaders(this._resHeaders);
  199. const age = this.age();
  200. // A cache SHOULD generate 113 warning if it heuristically chose a freshness
  201. // lifetime greater than 24 hours and the response's age is greater than 24 hours.
  202. if (age > 3600*24 && !this._hasExplicitExpiration() && this.maxAge() > 3600*24) {
  203. headers.warning = (headers.warning ? `${headers.warning}, ` : '') + '113 - "rfc7234 5.5.4"';
  204. }
  205. headers.age = `${Math.round(age)}`;
  206. headers.date = new Date(this.now()).toUTCString();
  207. return headers;
  208. }
  209. /**
  210. * Value of the Date response header or current time if Date was demed invalid
  211. * @return timestamp
  212. */
  213. date() {
  214. if (this._trustServerDate) {
  215. return this._serverDate();
  216. }
  217. return this._responseTime;
  218. }
  219. _serverDate() {
  220. const dateValue = Date.parse(this._resHeaders.date)
  221. if (isFinite(dateValue)) {
  222. const maxClockDrift = 8*3600*1000;
  223. const clockDrift = Math.abs(this._responseTime - dateValue);
  224. if (clockDrift < maxClockDrift) {
  225. return dateValue;
  226. }
  227. }
  228. return this._responseTime;
  229. }
  230. /**
  231. * Value of the Age header, in seconds, updated for the current time.
  232. * May be fractional.
  233. *
  234. * @return Number
  235. */
  236. age() {
  237. let age = Math.max(0, (this._responseTime - this.date())/1000);
  238. if (this._resHeaders.age) {
  239. let ageValue = this._ageValue();
  240. if (ageValue > age) age = ageValue;
  241. }
  242. const residentTime = (this.now() - this._responseTime)/1000;
  243. return age + residentTime;
  244. }
  245. _ageValue() {
  246. const ageValue = parseInt(this._resHeaders.age);
  247. return isFinite(ageValue) ? ageValue : 0;
  248. }
  249. /**
  250. * Value of applicable max-age (or heuristic equivalent) in seconds. This counts since response's `Date`.
  251. *
  252. * For an up-to-date value, see `timeToLive()`.
  253. *
  254. * @return Number
  255. */
  256. maxAge() {
  257. if (!this.storable() || this._rescc['no-cache']) {
  258. return 0;
  259. }
  260. // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default
  261. // so this implementation requires explicit opt-in via public header
  262. if (this._isShared && (this._resHeaders['set-cookie'] && !this._rescc.public && !this._rescc.immutable)) {
  263. return 0;
  264. }
  265. if (this._resHeaders.vary === '*') {
  266. return 0;
  267. }
  268. if (this._isShared) {
  269. if (this._rescc['proxy-revalidate']) {
  270. return 0;
  271. }
  272. // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
  273. if (this._rescc['s-maxage']) {
  274. return parseInt(this._rescc['s-maxage'], 10);
  275. }
  276. }
  277. // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
  278. if (this._rescc['max-age']) {
  279. return parseInt(this._rescc['max-age'], 10);
  280. }
  281. const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
  282. const dateValue = this._serverDate();
  283. if (this._resHeaders.expires) {
  284. const expires = Date.parse(this._resHeaders.expires);
  285. // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
  286. if (Number.isNaN(expires) || expires < dateValue) {
  287. return 0;
  288. }
  289. return Math.max(defaultMinTtl, (expires - dateValue)/1000);
  290. }
  291. if (this._resHeaders['last-modified']) {
  292. const lastModified = Date.parse(this._resHeaders['last-modified']);
  293. if (isFinite(lastModified) && dateValue > lastModified) {
  294. return Math.max(defaultMinTtl, (dateValue - lastModified)/1000 * this._cacheHeuristic);
  295. }
  296. }
  297. return defaultMinTtl;
  298. }
  299. timeToLive() {
  300. return Math.max(0, this.maxAge() - this.age())*1000;
  301. }
  302. stale() {
  303. return this.maxAge() <= this.age();
  304. }
  305. static fromObject(obj) {
  306. return new this(undefined, undefined, {_fromObject:obj});
  307. }
  308. _fromObject(obj) {
  309. if (this._responseTime) throw Error("Reinitialized");
  310. if (!obj || obj.v !== 1) throw Error("Invalid serialization");
  311. this._responseTime = obj.t;
  312. this._isShared = obj.sh;
  313. this._cacheHeuristic = obj.ch;
  314. this._immutableMinTtl = obj.imm !== undefined ? obj.imm : 24*3600*1000;
  315. this._status = obj.st;
  316. this._resHeaders = obj.resh;
  317. this._rescc = obj.rescc;
  318. this._method = obj.m;
  319. this._url = obj.u;
  320. this._host = obj.h;
  321. this._noAuthorization = obj.a;
  322. this._reqHeaders = obj.reqh;
  323. this._reqcc = obj.reqcc;
  324. }
  325. toObject() {
  326. return {
  327. v:1,
  328. t: this._responseTime,
  329. sh: this._isShared,
  330. ch: this._cacheHeuristic,
  331. imm: this._immutableMinTtl,
  332. st: this._status,
  333. resh: this._resHeaders,
  334. rescc: this._rescc,
  335. m: this._method,
  336. u: this._url,
  337. h: this._host,
  338. a: this._noAuthorization,
  339. reqh: this._reqHeaders,
  340. reqcc: this._reqcc,
  341. };
  342. }
  343. /**
  344. * Headers for sending to the origin server to revalidate stale response.
  345. * Allows server to return 304 to allow reuse of the previous response.
  346. *
  347. * Hop by hop headers are always stripped.
  348. * Revalidation headers may be added or removed, depending on request.
  349. */
  350. revalidationHeaders(incomingReq) {
  351. this._assertRequestHasHeaders(incomingReq);
  352. const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers);
  353. // This implementation does not understand range requests
  354. delete headers['if-range'];
  355. if (!this._requestMatches(incomingReq, true) || !this.storable()) { // revalidation allowed via HEAD
  356. // not for the same resource, or wasn't allowed to be cached anyway
  357. delete headers['if-none-match'];
  358. delete headers['if-modified-since'];
  359. return headers;
  360. }
  361. /* 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. */
  362. if (this._resHeaders.etag) {
  363. headers['if-none-match'] = headers['if-none-match'] ? `${headers['if-none-match']}, ${this._resHeaders.etag}` : this._resHeaders.etag;
  364. }
  365. // 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.
  366. const forbidsWeakValidators = headers['accept-ranges'] || headers['if-match'] || headers['if-unmodified-since'] || (this._method && this._method != 'GET');
  367. /* 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.
  368. Note: This implementation does not understand partial responses (206) */
  369. if (forbidsWeakValidators) {
  370. delete headers['if-modified-since'];
  371. if (headers['if-none-match']) {
  372. const etags = headers['if-none-match'].split(/,/).filter(etag => {
  373. return !/^\s*W\//.test(etag);
  374. });
  375. if (!etags.length) {
  376. delete headers['if-none-match'];
  377. } else {
  378. headers['if-none-match'] = etags.join(',').trim();
  379. }
  380. }
  381. } else if (this._resHeaders['last-modified'] && !headers['if-modified-since']) {
  382. headers['if-modified-since'] = this._resHeaders['last-modified'];
  383. }
  384. return headers;
  385. }
  386. /**
  387. * Creates new CachePolicy with information combined from the previews response,
  388. * and the new revalidation response.
  389. *
  390. * Returns {policy, modified} where modified is a boolean indicating
  391. * whether the response body has been modified, and old cached body can't be used.
  392. *
  393. * @return {Object} {policy: CachePolicy, modified: Boolean}
  394. */
  395. revalidatedPolicy(request, response) {
  396. this._assertRequestHasHeaders(request);
  397. if (!response || !response.headers) {
  398. throw Error("Response headers missing");
  399. }
  400. // These aren't going to be supported exactly, since one CachePolicy object
  401. // doesn't know about all the other cached objects.
  402. let matches = false;
  403. if (response.status !== undefined && response.status != 304) {
  404. matches = false;
  405. } else if (response.headers.etag && !/^\s*W\//.test(response.headers.etag)) {
  406. // "All of the stored responses with the same strong validator are selected.
  407. // If none of the stored responses contain the same strong validator,
  408. // then the cache MUST NOT use the new response to update any stored responses."
  409. matches = this._resHeaders.etag && this._resHeaders.etag.replace(/^\s*W\//,'') === response.headers.etag;
  410. } else if (this._resHeaders.etag && response.headers.etag) {
  411. // "If the new response contains a weak validator and that validator corresponds
  412. // to one of the cache's stored responses,
  413. // then the most recent of those matching stored responses is selected for update."
  414. matches = this._resHeaders.etag.replace(/^\s*W\//,'') === response.headers.etag.replace(/^\s*W\//,'');
  415. } else if (this._resHeaders['last-modified']) {
  416. matches = this._resHeaders['last-modified'] === response.headers['last-modified'];
  417. } else {
  418. // If the new response does not include any form of validator (such as in the case where
  419. // a client generates an If-Modified-Since request from a source other than the Last-Modified
  420. // response header field), and there is only one stored response, and that stored response also
  421. // lacks a validator, then that stored response is selected for update.
  422. if (!this._resHeaders.etag && !this._resHeaders['last-modified'] &&
  423. !response.headers.etag && !response.headers['last-modified']) {
  424. matches = true;
  425. }
  426. }
  427. if (!matches) {
  428. return {
  429. policy: new this.constructor(request, response),
  430. modified: true,
  431. }
  432. }
  433. // use other header fields provided in the 304 (Not Modified) response to replace all instances
  434. // of the corresponding header fields in the stored response.
  435. const headers = {};
  436. for(const k in this._resHeaders) {
  437. headers[k] = k in response.headers && !excludedFromRevalidationUpdate[k] ? response.headers[k] : this._resHeaders[k];
  438. }
  439. const newResponse = Object.assign({}, response, {
  440. status: this._status,
  441. method: this._method,
  442. headers,
  443. });
  444. return {
  445. policy: new this.constructor(request, newResponse),
  446. modified: false,
  447. };
  448. }
  449. };