123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- 'use strict'
- const assert = require('assert')
- const { kHeadersList } = require('../core/symbols')
- function isCTLExcludingHtab (value) {
- if (value.length === 0) {
- return false
- }
- for (const char of value) {
- const code = char.charCodeAt(0)
- if (
- (code >= 0x00 || code <= 0x08) ||
- (code >= 0x0A || code <= 0x1F) ||
- code === 0x7F
- ) {
- return false
- }
- }
- }
- /**
- CHAR = <any US-ASCII character (octets 0 - 127)>
- token = 1*<any CHAR except CTLs or separators>
- separators = "(" | ")" | "<" | ">" | "@"
- | "," | ";" | ":" | "\" | <">
- | "/" | "[" | "]" | "?" | "="
- | "{" | "}" | SP | HT
- * @param {string} name
- */
- function validateCookieName (name) {
- for (const char of name) {
- const code = char.charCodeAt(0)
- if (
- (code <= 0x20 || code > 0x7F) ||
- char === '(' ||
- char === ')' ||
- char === '>' ||
- char === '<' ||
- char === '@' ||
- char === ',' ||
- char === ';' ||
- char === ':' ||
- char === '\\' ||
- char === '"' ||
- char === '/' ||
- char === '[' ||
- char === ']' ||
- char === '?' ||
- char === '=' ||
- char === '{' ||
- char === '}'
- ) {
- 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) {
- for (const char of value) {
- const code = char.charCodeAt(0)
- if (
- code < 0x21 || // exclude CTLs (0-31)
- code === 0x22 ||
- code === 0x2C ||
- code === 0x3B ||
- code === 0x5C ||
- code > 0x7E // non-ascii
- ) {
- throw new Error('Invalid header value')
- }
- }
- }
- /**
- * path-value = <any CHAR except CTLs or ";">
- * @param {string} path
- */
- function validateCookiePath (path) {
- for (const char of path) {
- const code = char.charCodeAt(0)
- if (code < 0x21 || char === ';') {
- 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')
- }
- }
- /**
- * @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)
- }
- const days = [
- 'Sun', 'Mon', 'Tue', 'Wed',
- 'Thu', 'Fri', 'Sat'
- ]
- const months = [
- 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
- ]
- const dayName = days[date.getUTCDay()]
- const day = date.getUTCDate().toString().padStart(2, '0')
- const month = months[date.getUTCMonth()]
- const year = date.getUTCFullYear()
- const hour = date.getUTCHours().toString().padStart(2, '0')
- const minute = date.getUTCMinutes().toString().padStart(2, '0')
- const second = date.getUTCSeconds().toString().padStart(2, '0')
- return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} 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('; ')
- }
- let kHeadersListNode
- function getHeadersList (headers) {
- if (headers[kHeadersList]) {
- return headers[kHeadersList]
- }
- if (!kHeadersListNode) {
- kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
- (symbol) => symbol.description === 'headers list'
- )
- assert(kHeadersListNode, 'Headers cannot be parsed')
- }
- const headersList = headers[kHeadersListNode]
- assert(headersList)
- return headersList
- }
- module.exports = {
- isCTLExcludingHtab,
- stringify,
- getHeadersList
- }
|