'use strict'; /** * @param {string} value * @returns {boolean} */ function isCTLExcludingHtab(value) { for (let i = 0; i < value.length; ++i) { const code = value.charCodeAt(i); if ( (code >= 0x00 && code <= 0x08) || (code >= 0x0a && code <= 0x1f) || code === 0x7f ) { return true; } } return false; } /** CHAR = token = 1* separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT * @param {string} name */ function validateCookieName(name) { for (let i = 0; i < name.length; ++i) { const code = name.charCodeAt(i); if ( code < 0x21 || // exclude CTLs (0-31), SP and HT code > 0x7e || // exclude non-ascii and DEL code === 0x22 || // " code === 0x28 || // ( code === 0x29 || // ) code === 0x3c || // < code === 0x3e || // > code === 0x40 || // @ code === 0x2c || // , code === 0x3b || // ; code === 0x3a || // : code === 0x5c || // \ code === 0x2f || // / code === 0x5b || // [ code === 0x5d || // ] code === 0x3f || // ? code === 0x3d || // = code === 0x7b || // { code === 0x7d // } ) { throw new Error('Invalid cookie name'); } } } /** cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E ; US-ASCII characters excluding CTLs, ; whitespace DQUOTE, comma, semicolon, ; and backslash * @param {string} value */ function validateCookieValue(value) { let len = value.length; let i = 0; // if the value is wrapped in DQUOTE if (value[0] === '"') { if (len === 1 || value[len - 1] !== '"') { throw new Error('Invalid cookie value'); } --len; ++i; } while (i < len) { const code = value.charCodeAt(i++); if ( code < 0x21 || // exclude CTLs (0-31) code > 0x7e || // non-ascii and DEL (127) code === 0x22 || // " code === 0x2c || // , code === 0x3b || // ; code === 0x5c // \ ) { throw new Error('Invalid cookie value'); } } } /** * path-value = * @param {string} path */ function validateCookiePath(path) { for (let i = 0; i < path.length; ++i) { const code = path.charCodeAt(i); if ( code < 0x20 || // exclude CTLs (0-31) code === 0x7f || // DEL code === 0x3b // ; ) { throw new Error('Invalid cookie path'); } } } /** * I have no idea why these values aren't allowed to be honest, * but Deno tests these. - Khafra * @param {string} domain */ function validateCookieDomain(domain) { if (domain.startsWith('-') || domain.endsWith('.') || domain.endsWith('-')) { throw new Error('Invalid cookie domain'); } } const IMFDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const IMFMonths = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; const IMFPaddedNumbers = Array(61) .fill(0) .map((_, i) => i.toString().padStart(2, '0')); /** * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 * @param {number|Date} date IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT ; fixed length/zone/capitalization subset of the format ; see Section 3.3 of [RFC5322] day-name = %x4D.6F.6E ; "Mon", case-sensitive / %x54.75.65 ; "Tue", case-sensitive / %x57.65.64 ; "Wed", case-sensitive / %x54.68.75 ; "Thu", case-sensitive / %x46.72.69 ; "Fri", case-sensitive / %x53.61.74 ; "Sat", case-sensitive / %x53.75.6E ; "Sun", case-sensitive date1 = day SP month SP year ; e.g., 02 Jun 1982 day = 2DIGIT month = %x4A.61.6E ; "Jan", case-sensitive / %x46.65.62 ; "Feb", case-sensitive / %x4D.61.72 ; "Mar", case-sensitive / %x41.70.72 ; "Apr", case-sensitive / %x4D.61.79 ; "May", case-sensitive / %x4A.75.6E ; "Jun", case-sensitive / %x4A.75.6C ; "Jul", case-sensitive / %x41.75.67 ; "Aug", case-sensitive / %x53.65.70 ; "Sep", case-sensitive / %x4F.63.74 ; "Oct", case-sensitive / %x4E.6F.76 ; "Nov", case-sensitive / %x44.65.63 ; "Dec", case-sensitive year = 4DIGIT GMT = %x47.4D.54 ; "GMT", case-sensitive time-of-day = hour ":" minute ":" second ; 00:00:00 - 23:59:60 (leap second) hour = 2DIGIT minute = 2DIGIT second = 2DIGIT */ function toIMFDate(date) { if (typeof date === 'number') { date = new Date(date); } return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT`; } /** max-age-av = "Max-Age=" non-zero-digit *DIGIT ; In practice, both expires-av and max-age-av ; are limited to dates representable by the ; user agent. * @param {number} maxAge */ function validateCookieMaxAge(maxAge) { if (maxAge < 0) { throw new Error('Invalid cookie max-age'); } } /** * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 * @param {import('./index').Cookie} cookie */ function stringify(cookie) { if (cookie.name.length === 0) { return null; } validateCookieName(cookie.name); validateCookieValue(cookie.value); const out = [`${cookie.name}=${cookie.value}`]; // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2 if (cookie.name.startsWith('__Secure-')) { cookie.secure = true; } if (cookie.name.startsWith('__Host-')) { cookie.secure = true; cookie.domain = null; cookie.path = '/'; } if (cookie.secure) { out.push('Secure'); } if (cookie.httpOnly) { out.push('HttpOnly'); } if (typeof cookie.maxAge === 'number') { validateCookieMaxAge(cookie.maxAge); out.push(`Max-Age=${cookie.maxAge}`); } if (cookie.domain) { validateCookieDomain(cookie.domain); out.push(`Domain=${cookie.domain}`); } if (cookie.path) { validateCookiePath(cookie.path); out.push(`Path=${cookie.path}`); } if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { out.push(`Expires=${toIMFDate(cookie.expires)}`); } if (cookie.sameSite) { out.push(`SameSite=${cookie.sameSite}`); } for (const part of cookie.unparsed) { if (!part.includes('=')) { throw new Error('Invalid unparsed'); } const [key, ...value] = part.split('='); out.push(`${key.trim()}=${value.join('=')}`); } return out.join('; '); } module.exports = { isCTLExcludingHtab, validateCookieName, validateCookiePath, validateCookieValue, toIMFDate, stringify, };