X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Fcommon.js;h=2f0aeb835694b5eb0f5e6130b8974ff645c585b6;hb=776161ca80ca5a604199e43dfcf0ce06ef4fc1eb;hp=c6c21efc705cf7581dfd351ffe8ba3a8275cbdb1;hpb=1296abc51df4a92760dde53902c0043908276811;p=squeep-api-dingus diff --git a/lib/common.js b/lib/common.js index c6c21ef..2f0aeb8 100644 --- a/lib/common.js +++ b/lib/common.js @@ -205,6 +205,12 @@ const unfoldHeaderLines = (lines) => { return lines; }; +const validTokenRE = /^[!#$%&'*+-.0-9A-Z^_`a-z~]+$/; +const validValueRE = /^[!#$%&'()*+-./0-9:<=>?@A-Z[\]^_`a-z{|}~]*$/; +const validPathRE = /^[ !"#$%&'()*+,-./0-9:<=>?@A-Z[\\\]^_`a-z{|}~]*$/; +const validLabelRE = /^[a-zA-Z0-9-]+$/; +const invalidLabelRE = /--|^-|-$/; + /** * Adds a new set-cookie header value to response, with supplied data. * @param {http.ServerResponse} res response @@ -219,6 +225,7 @@ const unfoldHeaderLines = (lines) => { * @param {string=} opt.path cookie path * @param {string=} opt.sameSite cookie sharing * @param {boolean=} opt.secure cookie security + * @param {string[]=} opt.extension cookie extension attribute values */ function addCookie(res, name, value, opt = {}) { const options = { @@ -229,39 +236,83 @@ function addCookie(res, name, value, opt = {}) { path: undefined, sameSite: undefined, secure: false, + extension: [], ...opt, }; - // TODO: validate name, value + + if (!validTokenRE.test(name)) { + throw new RangeError('invalid cookie name'); + } + + if (value.startsWith('"') && value.endsWith('"')) { + if (!validValueRE.test(value.slice(1, value.length - 1))) { + throw new RangeError('invalid cookie value'); + }; + } else if (!validValueRE.test(value)) { + throw new RangeError('invalid cookie value'); + } + const cookieParts = [ `${name}=${value}`, ]; + if (options.domain) { + for (const label of options.domain.split('.')) { + if (!validLabelRE.test(label) || invalidLabelRE.test(label)) { + throw new RangeError('invalid cookie domain'); + } + + } cookieParts.push(`Domain=${options.domain}`); } + if (options.expires) { if (!(options.expires instanceof Date)) { throw new TypeError('cookie expires must be Date'); } cookieParts.push(`Expires=${options.expires.toUTCString()}`); } + if (options.httpOnly) { cookieParts.push('HttpOnly'); } + if (options.maxAge) { cookieParts.push(`Max-Age=${options.maxAge}`); } + if (options.path) { + if (!validPathRE.test(options.path)) { + throw new RangeError('cookie path value not valid'); + } cookieParts.push(`Path=${options.path}`); } + if (options.sameSite) { if (!(['Strict', 'Lax', 'None'].includes(options.sameSite))) { throw new RangeError('cookie sameSite value not valid'); } + if (options.sameSite === 'None' + && !options.secure) { + throw new RangeError('cookie with sameSite None must also be secure'); + } cookieParts.push(`SameSite=${options.sameSite}`); } + if (options.secure) { cookieParts.push('Secure'); } + + if (!Array.isArray(options.extension)) { + throw new TypeError('cookie extension must be Array'); + } + for (const extension of options.extension) { + if (!validPathRE.test(extension)) { + throw new RangeError('cookie extension value not valid'); + } + cookieParts.push(extension); + } + res.appendHeader(Enum.Header.SetCookie, cookieParts.join('; ')); }