@midwayjs/cookies
Version:
midway cookies
182 lines • 6.93 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.urlSafeDecode = exports.urlSafeEncode = exports.Cookies = void 0;
const assert = require("assert");
const cookie_1 = require("./cookie");
const keygrip_1 = require("./keygrip");
const should_send_same_site_none_1 = require("should-send-same-site-none");
const KEYS_ARRAY = Symbol('midwayCookies:keysArray');
const KEYS = Symbol('midwayCookies:keys');
const keyCache = new Map();
/**
* cookies extend pillarjs/cookies, add encrypt and decrypt
*/
class Cookies {
constructor(ctx, keys, defaultCookieOptions) {
var _a;
this[KEYS_ARRAY] = keys ? [].concat(keys) : keys;
// default cookie options
this._defaultCookieOptions = defaultCookieOptions;
this.ctx = ctx;
this.secure = (_a = defaultCookieOptions === null || defaultCookieOptions === void 0 ? void 0 : defaultCookieOptions.secure) !== null && _a !== void 0 ? _a : 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_1.Keygrip(this[KEYS_ARRAY]);
keyCache.set(keysArray, this[KEYS]);
}
}
return this[KEYS];
}
/**
* This extracts the cookie with the given name from the
* Cookie header in the request. If such a cookie exists,
* its value is returned. Otherwise, nothing is returned.
* @param name The cookie's unique name.
* @param opts Optional. The options for cookie's getting.
* @returns The cookie's value according to the specific name.
*/
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 = urlSafeDecode(value);
const res = this.keys.decrypt(value);
if (res === null || res === void 0 ? void 0 : res.value) {
return res.value.toString();
}
return undefined;
}
set(name, value, opts) {
if (!opts && typeof value !== 'string') {
opts = value;
value = '';
}
opts = Object.assign({}, this._defaultCookieOptions, 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 && urlSafeEncode(this.keys.encrypt(value));
}
// 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_1.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 (0, should_send_same_site_none_1.isSameSiteNoneCompatible)(userAgent);
}
}
exports.Cookies = Cookies;
// https://github.com/linsight/should-send-same-site-none/blob/master/index.js#L86
function parseChromiumAndMajorVersion(userAgent) {
const m = /Chrom[^ /]+\/(\d+)[.\d]* /.exec(userAgent);
if (!m)
return { chromium: false, version: null };
// Extract digits from first capturing group.
return { chromium: true, majorVersion: parseInt(m[1]) };
}
const patternCache = new Map();
function getPattern(name) {
const cache = patternCache.get(name);
if (cache)
return cache;
const reg = new RegExp('(?:^|;) *' + name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '=([^;]*)');
patternCache.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;
}
function urlSafeEncode(encode) {
return encode.replace(/\+/g, '-').replace(/\//g, '_');
}
exports.urlSafeEncode = urlSafeEncode;
function urlSafeDecode(encodeStr) {
return encodeStr.replace(/-/g, '+').replace(/_/g, '/');
}
exports.urlSafeDecode = urlSafeDecode;
//# sourceMappingURL=cookies.js.map