UNPKG

@midwayjs/cookies

Version:
182 lines 6.93 kB
"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