UNPKG

@midwayjs/cookies

Version:
246 lines 9.77 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, defaultGetCookieOptions) { var _a; this[KEYS_ARRAY] = keys ? [].concat(keys) : keys; // default cookie options this._defaultCookieOptions = defaultCookieOptions; // default get cookie options this._defaultGetCookieOptions = defaultGetCookieOptions; 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 = Object.assign({}, this._defaultGetCookieOptions || {}, 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 const userAgent = this.ctx.get('user-agent'); if (opts.sameSite && typeof opts.sameSite === 'string' && opts.sameSite.toLowerCase() === 'none') { if (!this.secure || (userAgent && !this.isSameSiteNoneCompatible(userAgent))) { // Non-secure context or Incompatible clients, don't send SameSite=None property opts.sameSite = false; } } if (opts.partitioned) { if (!this.secure || !userAgent || (userAgent && !this.isPartitionedCompatible(userAgent))) { // ignore partitioned when not secure or incompatible clients opts.partitioned = false; } } // remove unpartitioned same name cookie first if (opts.partitioned && opts.removeUnpartitioned) { const overwrite = opts.overwrite; if (overwrite) { opts.overwrite = false; headers = ignoreCookiesByName(headers, name); } const removeCookieOpts = Object.assign({}, opts, { partitioned: false, }); const removeUnpartitionedCookie = new cookie_1.Cookie(name, '', removeCookieOpts); // if user not set secure, reset secure to ctx.secure if (opts.secure === undefined) removeUnpartitionedCookie.attrs.secure = this.secure; headers = pushCookie(headers, removeUnpartitionedCookie); // signed if (signed) { removeUnpartitionedCookie.name += '.sig'; headers = ignoreCookiesByName(headers, removeUnpartitionedCookie.name); headers = pushCookie(headers, removeUnpartitionedCookie); } } if (opts.priority) { if (!userAgent || (userAgent && !this.isPriorityCompatible(userAgent))) { // ignore priority when not secure or incompatible clients opts.priority = undefined; } } 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 = this.parseChromiumAndMajorVersion(userAgent); if (result.chromium && result.majorVersion) { return result.majorVersion >= 80; } return (0, should_send_same_site_none_1.isSameSiteNoneCompatible)(userAgent); } isPartitionedCompatible(userAgent) { // Chrome & Edge >= 114.0.0.0 // https://developers.google.com/privacy-sandbox/3pcd/chips const result = this.parseChromiumAndMajorVersion(userAgent); if (result.chromium && result.majorVersion) { return result.majorVersion >= 114; } return false; } parseChromiumAndMajorVersion(userAgent) { // https://github.com/linsight/should-send-same-site-none/blob/master/index.js#L86 if (!this.uaParseResult) { const m = /Chrom[^ /]+\/(\d+)[.\d]* /.exec(userAgent); if (!m) return { chromium: false, majorVersion: undefined }; // Extract digits from first capturing group. this.uaParseResult = { chromium: true, majorVersion: parseInt(m[1]) }; } return this.uaParseResult; } isPriorityCompatible(userAgent) { // Chrome >= 81.0.0.0 // https://developer.chrome.com/blog/new-in-devtools-81?hl=zh-cn#cookiepriority const result = this.parseChromiumAndMajorVersion(userAgent); if (result.chromium && result.majorVersion) { return result.majorVersion >= 81; } return false; } } exports.Cookies = Cookies; 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 = ignoreCookiesByName(cookies, cookie.name); } cookies.push(cookie.toHeader()); return cookies; } function ignoreCookiesByName(cookies, name) { const prefix = `${name}=`; return cookies.filter(c => !c.startsWith(prefix)); } 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