parse.js 12 KB

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