cookieManager.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. // "use strict" is not required in ES6 modules as they are always in strict mode
  2. // Exporting functions and constants as named exports and default export
  3. /**
  4. * RegExp to match cookie-name in RFC 6265 sec 4.1.1
  5. * This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
  6. * which has been replaced by the token definition in RFC 7230 appendix B.
  7. *
  8. * cookie-name = token
  9. * token = 1*tchar
  10. * tchar = "!" / "#" / "$" / "%" / "&" / "'" /
  11. * "*" / "+" / "-" / "." / "^" / "_" /
  12. * "`" / "|" / "~" / DIGIT / ALPHA
  13. *
  14. * Note: Allowing more characters - https://github.com/jshttp/cookie/issues/191
  15. * Allow same range as cookie value, except `=`, which delimits end of name.
  16. */
  17. const cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
  18. /**
  19. * RegExp to match cookie-value in RFC 6265 sec 4.1.1
  20. *
  21. * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
  22. * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
  23. * ; US-ASCII characters excluding CTLs,
  24. * ; whitespace DQUOTE, comma, semicolon,
  25. * ; and backslash
  26. *
  27. * Allowing more characters: https://github.com/jshttp/cookie/issues/191
  28. * Comma, backslash, and DQUOTE are not part of the parsing algorithm.
  29. */
  30. const cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
  31. /**
  32. * RegExp to match domain-value in RFC 6265 sec 4.1.1
  33. *
  34. * domain-value = <subdomain>
  35. * ; defined in [RFC1034], Section 3.5, as
  36. * ; enhanced by [RFC1123], Section 2.1
  37. * <subdomain> = <label> | <subdomain> "." <label>
  38. * <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
  39. * Labels must be 63 characters or less.
  40. * 'let-dig' not 'letter' in the first char, per RFC1123
  41. * <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
  42. * <let-dig-hyp> = <let-dig> | "-"
  43. * <let-dig> = <letter> | <digit>
  44. * <letter> = any one of the 52 alphabetic characters A through Z in
  45. * upper case and a through z in lower case
  46. * <digit> = any one of the ten digits 0 through 9
  47. *
  48. * Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
  49. *
  50. * > (Note that a leading %x2E ("."), if present, is ignored even though that
  51. * character is not permitted, but a trailing %x2E ("."), if present, will
  52. * cause the user agent to ignore the attribute.)
  53. */
  54. const domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
  55. /**
  56. * RegExp to match path-value in RFC 6265 sec 4.1.1
  57. *
  58. * path-value = <any CHAR except CTLs or ";">
  59. * CHAR = %x01-7F
  60. * ; defined in RFC 5234 appendix B.1
  61. */
  62. const pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
  63. const __toString = Object.prototype.toString;
  64. const NullObject = (() => {
  65. const C = function () {
  66. };
  67. C.prototype = Object.create(null);
  68. return C;
  69. })();
  70. /**
  71. * Parse a cookie header.
  72. *
  73. * Parse the given cookie header string into an object
  74. * The object has the various cookies as keys(names) => values
  75. */
  76. export function parse(input, options) {
  77. const obj = new NullObject();
  78. const dec = options?.decode || decode;
  79. // If input is an array, process each string
  80. const headers = Array.isArray(input) ? input : [input];
  81. // console.log(headers);
  82. for (const str of headers) {
  83. const len = str.length;
  84. if (len < 2) continue;
  85. let index = 0;
  86. do {
  87. const eqIdx = str.indexOf("=", index);
  88. if (eqIdx === -1) break;
  89. const colonIdx = str.indexOf(";", index);
  90. const endIdx = colonIdx === -1 ? len : colonIdx;
  91. if (eqIdx > endIdx) {
  92. index = str.lastIndexOf(";", eqIdx - 1) + 1;
  93. continue;
  94. }
  95. const keyStartIdx = startIndex(str, index, eqIdx);
  96. const keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
  97. const key = str.slice(keyStartIdx, keyEndIdx); // Keep original case
  98. let valStartIdx = startIndex(str, eqIdx + 1, endIdx);
  99. let valEndIdx = endIndex(str, endIdx, valStartIdx);
  100. const value = dec(str.slice(valStartIdx, valEndIdx));
  101. // Always overwrite with the latest value for the same key
  102. obj[key] = value;
  103. index = endIdx + 1;
  104. } while (index < len);
  105. }
  106. return obj;
  107. }
  108. function startIndex(str, index, max) {
  109. do {
  110. const code = str.charCodeAt(index);
  111. if (code !== 0x20 && code !== 0x09) return index;
  112. } while (++index < max);
  113. return max;
  114. }
  115. function endIndex(str, index, min) {
  116. while (index > min) {
  117. const code = str.charCodeAt(--index);
  118. if (code !== 0x20 && code !== 0x09) return index + 1;
  119. }
  120. return min;
  121. }
  122. /**
  123. * Serialize data into a cookie header.
  124. *
  125. * Serialize a name value pair into a cookie string suitable for
  126. * http headers. An optional options object specifies cookie parameters.
  127. */
  128. export function serialize(name, val, options) {
  129. const enc = options?.encode || encodeURIComponent;
  130. if (!cookieNameRegExp.test(name)) {
  131. throw new TypeError(`argument name is invalid: ${name}`);
  132. }
  133. const value = enc(val);
  134. if (!cookieValueRegExp.test(value)) {
  135. throw new TypeError(`argument val is invalid: ${val}`);
  136. }
  137. let str = name + "=" + value;
  138. if (!options) return str;
  139. if (options.maxAge !== undefined) {
  140. if (!Number.isInteger(options.maxAge)) {
  141. throw new TypeError(`option maxAge is invalid: ${options.maxAge}`);
  142. }
  143. str += "; Max-Age=" + options.maxAge;
  144. }
  145. if (options.domain) {
  146. if (!domainValueRegExp.test(options.domain)) {
  147. throw new TypeError(`option domain is invalid: ${options.domain}`);
  148. }
  149. str += "; Domain=" + options.domain;
  150. }
  151. if (options.path) {
  152. if (!pathValueRegExp.test(options.path)) {
  153. throw new TypeError(`option path is invalid: ${options.path}`);
  154. }
  155. str += "; Path=" + options.path;
  156. }
  157. if (options.expires) {
  158. if (!isDate(options.expires) || !Number.isFinite(options.expires.valueOf())) {
  159. throw new TypeError(`option expires is invalid: ${options.expires}`);
  160. }
  161. str += "; Expires=" + options.expires.toUTCString();
  162. }
  163. if (options.httpOnly) {
  164. str += "; HttpOnly";
  165. }
  166. if (options.secure) {
  167. str += "; Secure";
  168. }
  169. if (options.partitioned) {
  170. str += "; Partitioned";
  171. }
  172. if (options.priority) {
  173. const priority = typeof options.priority === "string" ? options.priority.toLowerCase() : undefined;
  174. switch (priority) {
  175. case "low":
  176. str += "; Priority=Low";
  177. break;
  178. case "medium":
  179. str += "; Priority=Medium";
  180. break;
  181. case "high":
  182. str += "; Priority=High";
  183. break;
  184. default:
  185. throw new TypeError(`option priority is invalid: ${options.priority}`);
  186. }
  187. }
  188. if (options.sameSite) {
  189. const sameSite = typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite;
  190. switch (sameSite) {
  191. case true:
  192. case "strict":
  193. str += "; SameSite=Strict";
  194. break;
  195. case "lax":
  196. str += "; SameSite=Lax";
  197. break;
  198. case "none":
  199. str += "; SameSite=None";
  200. break;
  201. default:
  202. throw new TypeError(`option sameSite is invalid: ${options.sameSite}`);
  203. }
  204. }
  205. return str;
  206. }
  207. /**
  208. * URL-decode string value. Optimized to skip native call when no %.
  209. */
  210. function decode(str) {
  211. if (str.indexOf("%") === -1) return str;
  212. try {
  213. return decodeURIComponent(str);
  214. } catch (e) {
  215. return str;
  216. }
  217. }
  218. /**
  219. * Determine if value is a Date.
  220. */
  221. function isDate(val) {
  222. return __toString.call(val) === "[object Date]";
  223. }
  224. export function stringify(obj, encode = 0) {
  225. return Object.entries(obj)
  226. .map(([key, value]) => encode ? serialize(key, value) : decodeURIComponent(serialize(key, value)))
  227. .join("; ")
  228. }
  229. // Export as default object for named and default usage
  230. const COOKIE = {parse, serialize, stringify};
  231. export default COOKIE;