123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- 'use strict';
- const assert = require('assert');
- const utility = require('utility');
- const _isSameSiteNoneCompatible = require('should-send-same-site-none').isSameSiteNoneCompatible;
- const Keygrip = require('./keygrip');
- const Cookie = require('./cookie');
- const CookieError = require('./error');
- const KEYS_ARRAY = Symbol('eggCookies:keysArray');
- const KEYS = Symbol('eggCookies:keys');
- const keyCache = new Map();
- /**
- * cookies for egg
- * extend pillarjs/cookies, add encrypt and decrypt
- */
- class Cookies {
- constructor(ctx, keys, defaultCookieOptions) {
- this[KEYS_ARRAY] = keys;
- this._keys = keys;
- // default cookie options
- this._defaultCookieOptions = defaultCookieOptions;
- this.ctx = ctx;
- this.secure = this.ctx.secure;
- this.app = ctx.app;
- }
- get keys() {
- if (!this[KEYS]) {
- const keysArray = this[KEYS_ARRAY];
- assert(Array.isArray(keysArray), '.keys required for encrypt/sign cookies');
- const cache = keyCache.get(keysArray);
- if (cache) {
- this[KEYS] = cache;
- } else {
- this[KEYS] = new Keygrip(this[KEYS_ARRAY]);
- keyCache.set(keysArray, this[KEYS]);
- }
- }
- return this[KEYS];
- }
- /**
- * get cookie value by name
- * @param {String} name - cookie's name
- * @param {Object} opts - cookies' options
- * - {Boolean} signed - default to true
- * - {Boolean} encrypt - default to false
- * @return {String} value - cookie's value
- */
- get(name, opts) {
- opts = opts || {};
- const signed = computeSigned(opts);
- const header = this.ctx.get('cookie');
- if (!header) return;
- const match = header.match(getPattern(name));
- if (!match) return;
- let value = match[1];
- if (!opts.encrypt && !signed) return value;
- // signed
- if (signed) {
- const sigName = name + '.sig';
- const sigValue = this.get(sigName, { signed: false });
- if (!sigValue) return;
- const raw = name + '=' + value;
- const index = this.keys.verify(raw, sigValue);
- if (index < 0) {
- // can not match any key, remove ${name}.sig
- this.set(sigName, null, { path: '/', signed: false, overwrite: true });
- return;
- }
- if (index > 0) {
- // not signed by the first key, update sigValue
- this.set(sigName, this.keys.sign(raw), { signed: false, overwrite: true });
- }
- return value;
- }
- // encrypt
- value = utility.base64decode(value, true, 'buffer');
- const res = this.keys.decrypt(value);
- return res ? res.value.toString() : undefined;
- }
- set(name, value, opts) {
- opts = Object.assign({}, this._defaultCookieOptions, opts);
- const signed = computeSigned(opts);
- value = value || '';
- if (!this.secure && opts.secure) {
- throw new CookieError('Cannot send secure cookie over unencrypted connection');
- }
- let headers = this.ctx.response.get('set-cookie') || [];
- if (!Array.isArray(headers)) headers = [ headers ];
- // encrypt
- if (opts.encrypt) {
- value = value && utility.base64encode(this.keys.encrypt(value), true);
- }
- // http://browsercookielimits.squawky.net/
- if (value.length > 4093) {
- this.app.emit('cookieLimitExceed', { name, value, ctx: this.ctx });
- }
- // https://github.com/linsight/should-send-same-site-none
- // fixed SameSite=None: Known Incompatible Clients
- if (opts.sameSite && typeof opts.sameSite === 'string' && opts.sameSite.toLowerCase() === 'none') {
- const userAgent = this.ctx.get('user-agent');
- if (!this.secure || (userAgent && !this.isSameSiteNoneCompatible(userAgent))) {
- // Non-secure context or Incompatible clients, don't send SameSite=None property
- opts.sameSite = false;
- }
- }
- const cookie = new Cookie(name, value, opts);
- // if user not set secure, reset secure to ctx.secure
- if (opts.secure === undefined) cookie.attrs.secure = this.secure;
- headers = pushCookie(headers, cookie);
- // signed
- if (signed) {
- cookie.value = value && this.keys.sign(cookie.toString());
- cookie.name += '.sig';
- headers = pushCookie(headers, cookie);
- }
- this.ctx.set('set-cookie', headers);
- return this;
- }
- isSameSiteNoneCompatible(userAgent) {
- // Chrome >= 80.0.0.0
- const result = parseChromiumAndMajorVersion(userAgent);
- if (result.chromium) return result.majorVersion >= 80;
- return _isSameSiteNoneCompatible(userAgent);
- }
- }
- // https://github.com/linsight/should-send-same-site-none/blob/master/index.js#L86
- function parseChromiumAndMajorVersion(userAgent) {
- const m = /Chrom[^ \/]{1,100}\/(\d{1,100}?)\./.exec(userAgent);
- if (!m) return { chromium: false, version: null };
- // Extract digits from first capturing group.
- return { chromium: true, majorVersion: parseInt(m[1]) };
- }
- const partternCache = new Map();
- function getPattern(name) {
- const cache = partternCache.get(name);
- if (cache) return cache;
- const reg = new RegExp(
- '(?:^|;) *' +
- name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') +
- '=([^;]*)'
- );
- partternCache.set(name, reg);
- return reg;
- }
- function computeSigned(opts) {
- // encrypt default to false, signed default to true.
- // disable singed when encrypt is true.
- if (opts.encrypt) return false;
- return opts.signed !== false;
- }
- function pushCookie(cookies, cookie) {
- if (cookie.attrs.overwrite) {
- cookies = cookies.filter(c => !c.startsWith(cookie.name + '='));
- }
- cookies.push(cookie.toHeader());
- return cookies;
- }
- Cookies.CookieError = CookieError;
- module.exports = Cookies;
|