egg-cookies
Version:
cookies module for egg
158 lines (131 loc) • 4.13 kB
JavaScript
'use strict';
const assert = require('assert');
const utility = require('utility');
const Keygrip = require('./keygrip');
const Cookie = require('./cookie');
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) {
this[KEYS_ARRAY] = keys;
this._keys = keys;
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 });
return;
}
if (index > 0) {
// not signed by the first key, update sigValue
this.set(sigName, this.keys.sign(raw), { signed: false });
}
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 = opts || {};
const signed = computeSigned(opts);
value = value || '';
if (!this.secure && opts.secure) {
throw new Error('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 });
}
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;
}
}
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;
}
module.exports = Cookies;