• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const assert = require('assert')
4const { kHeadersList } = require('../core/symbols')
5
6function isCTLExcludingHtab (value) {
7  if (value.length === 0) {
8    return false
9  }
10
11  for (const char of value) {
12    const code = char.charCodeAt(0)
13
14    if (
15      (code >= 0x00 || code <= 0x08) ||
16      (code >= 0x0A || code <= 0x1F) ||
17      code === 0x7F
18    ) {
19      return false
20    }
21  }
22}
23
24/**
25 CHAR           = <any US-ASCII character (octets 0 - 127)>
26 token          = 1*<any CHAR except CTLs or separators>
27 separators     = "(" | ")" | "<" | ">" | "@"
28                | "," | ";" | ":" | "\" | <">
29                | "/" | "[" | "]" | "?" | "="
30                | "{" | "}" | SP | HT
31 * @param {string} name
32 */
33function validateCookieName (name) {
34  for (const char of name) {
35    const code = char.charCodeAt(0)
36
37    if (
38      (code <= 0x20 || code > 0x7F) ||
39      char === '(' ||
40      char === ')' ||
41      char === '>' ||
42      char === '<' ||
43      char === '@' ||
44      char === ',' ||
45      char === ';' ||
46      char === ':' ||
47      char === '\\' ||
48      char === '"' ||
49      char === '/' ||
50      char === '[' ||
51      char === ']' ||
52      char === '?' ||
53      char === '=' ||
54      char === '{' ||
55      char === '}'
56    ) {
57      throw new Error('Invalid cookie name')
58    }
59  }
60}
61
62/**
63 cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
64 cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
65                       ; US-ASCII characters excluding CTLs,
66                       ; whitespace DQUOTE, comma, semicolon,
67                       ; and backslash
68 * @param {string} value
69 */
70function validateCookieValue (value) {
71  for (const char of value) {
72    const code = char.charCodeAt(0)
73
74    if (
75      code < 0x21 || // exclude CTLs (0-31)
76      code === 0x22 ||
77      code === 0x2C ||
78      code === 0x3B ||
79      code === 0x5C ||
80      code > 0x7E // non-ascii
81    ) {
82      throw new Error('Invalid header value')
83    }
84  }
85}
86
87/**
88 * path-value        = <any CHAR except CTLs or ";">
89 * @param {string} path
90 */
91function validateCookiePath (path) {
92  for (const char of path) {
93    const code = char.charCodeAt(0)
94
95    if (code < 0x21 || char === ';') {
96      throw new Error('Invalid cookie path')
97    }
98  }
99}
100
101/**
102 * I have no idea why these values aren't allowed to be honest,
103 * but Deno tests these. - Khafra
104 * @param {string} domain
105 */
106function validateCookieDomain (domain) {
107  if (
108    domain.startsWith('-') ||
109    domain.endsWith('.') ||
110    domain.endsWith('-')
111  ) {
112    throw new Error('Invalid cookie domain')
113  }
114}
115
116/**
117 * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
118 * @param {number|Date} date
119  IMF-fixdate  = day-name "," SP date1 SP time-of-day SP GMT
120  ; fixed length/zone/capitalization subset of the format
121  ; see Section 3.3 of [RFC5322]
122
123  day-name     = %x4D.6F.6E ; "Mon", case-sensitive
124              / %x54.75.65 ; "Tue", case-sensitive
125              / %x57.65.64 ; "Wed", case-sensitive
126              / %x54.68.75 ; "Thu", case-sensitive
127              / %x46.72.69 ; "Fri", case-sensitive
128              / %x53.61.74 ; "Sat", case-sensitive
129              / %x53.75.6E ; "Sun", case-sensitive
130  date1        = day SP month SP year
131                  ; e.g., 02 Jun 1982
132
133  day          = 2DIGIT
134  month        = %x4A.61.6E ; "Jan", case-sensitive
135              / %x46.65.62 ; "Feb", case-sensitive
136              / %x4D.61.72 ; "Mar", case-sensitive
137              / %x41.70.72 ; "Apr", case-sensitive
138              / %x4D.61.79 ; "May", case-sensitive
139              / %x4A.75.6E ; "Jun", case-sensitive
140              / %x4A.75.6C ; "Jul", case-sensitive
141              / %x41.75.67 ; "Aug", case-sensitive
142              / %x53.65.70 ; "Sep", case-sensitive
143              / %x4F.63.74 ; "Oct", case-sensitive
144              / %x4E.6F.76 ; "Nov", case-sensitive
145              / %x44.65.63 ; "Dec", case-sensitive
146  year         = 4DIGIT
147
148  GMT          = %x47.4D.54 ; "GMT", case-sensitive
149
150  time-of-day  = hour ":" minute ":" second
151              ; 00:00:00 - 23:59:60 (leap second)
152
153  hour         = 2DIGIT
154  minute       = 2DIGIT
155  second       = 2DIGIT
156 */
157function toIMFDate (date) {
158  if (typeof date === 'number') {
159    date = new Date(date)
160  }
161
162  const days = [
163    'Sun', 'Mon', 'Tue', 'Wed',
164    'Thu', 'Fri', 'Sat'
165  ]
166
167  const months = [
168    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
169    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
170  ]
171
172  const dayName = days[date.getUTCDay()]
173  const day = date.getUTCDate().toString().padStart(2, '0')
174  const month = months[date.getUTCMonth()]
175  const year = date.getUTCFullYear()
176  const hour = date.getUTCHours().toString().padStart(2, '0')
177  const minute = date.getUTCMinutes().toString().padStart(2, '0')
178  const second = date.getUTCSeconds().toString().padStart(2, '0')
179
180  return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT`
181}
182
183/**
184 max-age-av        = "Max-Age=" non-zero-digit *DIGIT
185                       ; In practice, both expires-av and max-age-av
186                       ; are limited to dates representable by the
187                       ; user agent.
188 * @param {number} maxAge
189 */
190function validateCookieMaxAge (maxAge) {
191  if (maxAge < 0) {
192    throw new Error('Invalid cookie max-age')
193  }
194}
195
196/**
197 * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
198 * @param {import('./index').Cookie} cookie
199 */
200function stringify (cookie) {
201  if (cookie.name.length === 0) {
202    return null
203  }
204
205  validateCookieName(cookie.name)
206  validateCookieValue(cookie.value)
207
208  const out = [`${cookie.name}=${cookie.value}`]
209
210  // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
211  // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
212  if (cookie.name.startsWith('__Secure-')) {
213    cookie.secure = true
214  }
215
216  if (cookie.name.startsWith('__Host-')) {
217    cookie.secure = true
218    cookie.domain = null
219    cookie.path = '/'
220  }
221
222  if (cookie.secure) {
223    out.push('Secure')
224  }
225
226  if (cookie.httpOnly) {
227    out.push('HttpOnly')
228  }
229
230  if (typeof cookie.maxAge === 'number') {
231    validateCookieMaxAge(cookie.maxAge)
232    out.push(`Max-Age=${cookie.maxAge}`)
233  }
234
235  if (cookie.domain) {
236    validateCookieDomain(cookie.domain)
237    out.push(`Domain=${cookie.domain}`)
238  }
239
240  if (cookie.path) {
241    validateCookiePath(cookie.path)
242    out.push(`Path=${cookie.path}`)
243  }
244
245  if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
246    out.push(`Expires=${toIMFDate(cookie.expires)}`)
247  }
248
249  if (cookie.sameSite) {
250    out.push(`SameSite=${cookie.sameSite}`)
251  }
252
253  for (const part of cookie.unparsed) {
254    if (!part.includes('=')) {
255      throw new Error('Invalid unparsed')
256    }
257
258    const [key, ...value] = part.split('=')
259
260    out.push(`${key.trim()}=${value.join('=')}`)
261  }
262
263  return out.join('; ')
264}
265
266let kHeadersListNode
267
268function getHeadersList (headers) {
269  if (headers[kHeadersList]) {
270    return headers[kHeadersList]
271  }
272
273  if (!kHeadersListNode) {
274    kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
275      (symbol) => symbol.description === 'headers list'
276    )
277
278    assert(kHeadersListNode, 'Headers cannot be parsed')
279  }
280
281  const headersList = headers[kHeadersListNode]
282  assert(headersList)
283
284  return headersList
285}
286
287module.exports = {
288  isCTLExcludingHtab,
289  stringify,
290  getHeadersList
291}
292