1'use strict'
2
3const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
4const { isCTLExcludingHtab } = require('./util')
5const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL')
6const assert = require('assert')
7
8/**
9 * @description Parses the field-value attributes of a set-cookie header string.
10 * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
11 * @param {string} header
12 * @returns if the header is invalid, null will be returned
13 */
14function parseSetCookie (header) {
15  // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
16  //    character (CTL characters excluding HTAB): Abort these steps and
17  //    ignore the set-cookie-string entirely.
18  if (isCTLExcludingHtab(header)) {
19    return null
20  }
21
22  let nameValuePair = ''
23  let unparsedAttributes = ''
24  let name = ''
25  let value = ''
26
27  // 2. If the set-cookie-string contains a %x3B (";") character:
28  if (header.includes(';')) {
29    // 1. The name-value-pair string consists of the characters up to,
30    //    but not including, the first %x3B (";"), and the unparsed-
31    //    attributes consist of the remainder of the set-cookie-string
32    //    (including the %x3B (";") in question).
33    const position = { position: 0 }
34
35    nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
36    unparsedAttributes = header.slice(position.position)
37  } else {
38    // Otherwise:
39
40    // 1. The name-value-pair string consists of all the characters
41    //    contained in the set-cookie-string, and the unparsed-
42    //    attributes is the empty string.
43    nameValuePair = header
44  }
45
46  // 3. If the name-value-pair string lacks a %x3D ("=") character, then
47  //    the name string is empty, and the value string is the value of
48  //    name-value-pair.
49  if (!nameValuePair.includes('=')) {
50    value = nameValuePair
51  } else {
52    //    Otherwise, the name string consists of the characters up to, but
53    //    not including, the first %x3D ("=") character, and the (possibly
54    //    empty) value string consists of the characters after the first
55    //    %x3D ("=") character.
56    const position = { position: 0 }
57    name = collectASequenceOfCodePointsFast(
58      '=',
59      nameValuePair,
60      position
61    )
62    value = nameValuePair.slice(position.position + 1)
63  }
64
65  // 4. Remove any leading or trailing WSP characters from the name
66  //    string and the value string.
67  name = name.trim()
68  value = value.trim()
69
70  // 5. If the sum of the lengths of the name string and the value string
71  //    is more than 4096 octets, abort these steps and ignore the set-
72  //    cookie-string entirely.
73  if (name.length + value.length > maxNameValuePairSize) {
74    return null
75  }
76
77  // 6. The cookie-name is the name string, and the cookie-value is the
78  //    value string.
79  return {
80    name, value, ...parseUnparsedAttributes(unparsedAttributes)
81  }
82}
83
84/**
85 * Parses the remaining attributes of a set-cookie header
86 * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
87 * @param {string} unparsedAttributes
88 * @param {[Object.<string, unknown>]={}} cookieAttributeList
89 */
90function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
91  // 1. If the unparsed-attributes string is empty, skip the rest of
92  //    these steps.
93  if (unparsedAttributes.length === 0) {
94    return cookieAttributeList
95  }
96
97  // 2. Discard the first character of the unparsed-attributes (which
98  //    will be a %x3B (";") character).
99  assert(unparsedAttributes[0] === ';')
100  unparsedAttributes = unparsedAttributes.slice(1)
101
102  let cookieAv = ''
103
104  // 3. If the remaining unparsed-attributes contains a %x3B (";")
105  //    character:
106  if (unparsedAttributes.includes(';')) {
107    // 1. Consume the characters of the unparsed-attributes up to, but
108    //    not including, the first %x3B (";") character.
109    cookieAv = collectASequenceOfCodePointsFast(
110      ';',
111      unparsedAttributes,
112      { position: 0 }
113    )
114    unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
115  } else {
116    // Otherwise:
117
118    // 1. Consume the remainder of the unparsed-attributes.
119    cookieAv = unparsedAttributes
120    unparsedAttributes = ''
121  }
122
123  // Let the cookie-av string be the characters consumed in this step.
124
125  let attributeName = ''
126  let attributeValue = ''
127
128  // 4. If the cookie-av string contains a %x3D ("=") character:
129  if (cookieAv.includes('=')) {
130    // 1. The (possibly empty) attribute-name string consists of the
131    //    characters up to, but not including, the first %x3D ("=")
132    //    character, and the (possibly empty) attribute-value string
133    //    consists of the characters after the first %x3D ("=")
134    //    character.
135    const position = { position: 0 }
136
137    attributeName = collectASequenceOfCodePointsFast(
138      '=',
139      cookieAv,
140      position
141    )
142    attributeValue = cookieAv.slice(position.position + 1)
143  } else {
144    // Otherwise:
145
146    // 1. The attribute-name string consists of the entire cookie-av
147    //    string, and the attribute-value string is empty.
148    attributeName = cookieAv
149  }
150
151  // 5. Remove any leading or trailing WSP characters from the attribute-
152  //    name string and the attribute-value string.
153  attributeName = attributeName.trim()
154  attributeValue = attributeValue.trim()
155
156  // 6. If the attribute-value is longer than 1024 octets, ignore the
157  //    cookie-av string and return to Step 1 of this algorithm.
158  if (attributeValue.length > maxAttributeValueSize) {
159    return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
160  }
161
162  // 7. Process the attribute-name and attribute-value according to the
163  //    requirements in the following subsections.  (Notice that
164  //    attributes with unrecognized attribute-names are ignored.)
165  const attributeNameLowercase = attributeName.toLowerCase()
166
167  // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
168  // If the attribute-name case-insensitively matches the string
169  // "Expires", the user agent MUST process the cookie-av as follows.
170  if (attributeNameLowercase === 'expires') {
171    // 1. Let the expiry-time be the result of parsing the attribute-value
172    //    as cookie-date (see Section 5.1.1).
173    const expiryTime = new Date(attributeValue)
174
175    // 2. If the attribute-value failed to parse as a cookie date, ignore
176    //    the cookie-av.
177
178    cookieAttributeList.expires = expiryTime
179  } else if (attributeNameLowercase === 'max-age') {
180    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
181    // If the attribute-name case-insensitively matches the string "Max-
182    // Age", the user agent MUST process the cookie-av as follows.
183
184    // 1. If the first character of the attribute-value is not a DIGIT or a
185    //    "-" character, ignore the cookie-av.
186    const charCode = attributeValue.charCodeAt(0)
187
188    if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
189      return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
190    }
191
192    // 2. If the remainder of attribute-value contains a non-DIGIT
193    //    character, ignore the cookie-av.
194    if (!/^\d+$/.test(attributeValue)) {
195      return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
196    }
197
198    // 3. Let delta-seconds be the attribute-value converted to an integer.
199    const deltaSeconds = Number(attributeValue)
200
201    // 4. Let cookie-age-limit be the maximum age of the cookie (which
202    //    SHOULD be 400 days or less, see Section 4.1.2.2).
203
204    // 5. Set delta-seconds to the smaller of its present value and cookie-
205    //    age-limit.
206    // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
207
208    // 6. If delta-seconds is less than or equal to zero (0), let expiry-
209    //    time be the earliest representable date and time.  Otherwise, let
210    //    the expiry-time be the current date and time plus delta-seconds
211    //    seconds.
212    // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
213
214    // 7. Append an attribute to the cookie-attribute-list with an
215    //    attribute-name of Max-Age and an attribute-value of expiry-time.
216    cookieAttributeList.maxAge = deltaSeconds
217  } else if (attributeNameLowercase === 'domain') {
218    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
219    // If the attribute-name case-insensitively matches the string "Domain",
220    // the user agent MUST process the cookie-av as follows.
221
222    // 1. Let cookie-domain be the attribute-value.
223    let cookieDomain = attributeValue
224
225    // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
226    //    cookie-domain without its leading %x2E (".").
227    if (cookieDomain[0] === '.') {
228      cookieDomain = cookieDomain.slice(1)
229    }
230
231    // 3. Convert the cookie-domain to lower case.
232    cookieDomain = cookieDomain.toLowerCase()
233
234    // 4. Append an attribute to the cookie-attribute-list with an
235    //    attribute-name of Domain and an attribute-value of cookie-domain.
236    cookieAttributeList.domain = cookieDomain
237  } else if (attributeNameLowercase === 'path') {
238    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
239    // If the attribute-name case-insensitively matches the string "Path",
240    // the user agent MUST process the cookie-av as follows.
241
242    // 1. If the attribute-value is empty or if the first character of the
243    //    attribute-value is not %x2F ("/"):
244    let cookiePath = ''
245    if (attributeValue.length === 0 || attributeValue[0] !== '/') {
246      // 1. Let cookie-path be the default-path.
247      cookiePath = '/'
248    } else {
249      // Otherwise:
250
251      // 1. Let cookie-path be the attribute-value.
252      cookiePath = attributeValue
253    }
254
255    // 2. Append an attribute to the cookie-attribute-list with an
256    //    attribute-name of Path and an attribute-value of cookie-path.
257    cookieAttributeList.path = cookiePath
258  } else if (attributeNameLowercase === 'secure') {
259    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
260    // If the attribute-name case-insensitively matches the string "Secure",
261    // the user agent MUST append an attribute to the cookie-attribute-list
262    // with an attribute-name of Secure and an empty attribute-value.
263
264    cookieAttributeList.secure = true
265  } else if (attributeNameLowercase === 'httponly') {
266    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
267    // If the attribute-name case-insensitively matches the string
268    // "HttpOnly", the user agent MUST append an attribute to the cookie-
269    // attribute-list with an attribute-name of HttpOnly and an empty
270    // attribute-value.
271
272    cookieAttributeList.httpOnly = true
273  } else if (attributeNameLowercase === 'samesite') {
274    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
275    // If the attribute-name case-insensitively matches the string
276    // "SameSite", the user agent MUST process the cookie-av as follows:
277
278    // 1. Let enforcement be "Default".
279    let enforcement = 'Default'
280
281    const attributeValueLowercase = attributeValue.toLowerCase()
282    // 2. If cookie-av's attribute-value is a case-insensitive match for
283    //    "None", set enforcement to "None".
284    if (attributeValueLowercase.includes('none')) {
285      enforcement = 'None'
286    }
287
288    // 3. If cookie-av's attribute-value is a case-insensitive match for
289    //    "Strict", set enforcement to "Strict".
290    if (attributeValueLowercase.includes('strict')) {
291      enforcement = 'Strict'
292    }
293
294    // 4. If cookie-av's attribute-value is a case-insensitive match for
295    //    "Lax", set enforcement to "Lax".
296    if (attributeValueLowercase.includes('lax')) {
297      enforcement = 'Lax'
298    }
299
300    // 5. Append an attribute to the cookie-attribute-list with an
301    //    attribute-name of "SameSite" and an attribute-value of
302    //    enforcement.
303    cookieAttributeList.sameSite = enforcement
304  } else {
305    cookieAttributeList.unparsed ??= []
306
307    cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
308  }
309
310  // 8. Return to Step 1 of this algorithm.
311  return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
312}
313
314module.exports = {
315  parseSetCookie,
316  parseUnparsedAttributes
317}
318