parse.js 12 KB

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