set-cookie.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. "use strict";
  2. var defaultParseOptions = {
  3. decodeValues: true,
  4. map: false,
  5. silent: false,
  6. };
  7. function isForbiddenKey(key) {
  8. return typeof key !== "string" || key in {};
  9. }
  10. function createNullObj() {
  11. return Object.create(null);
  12. }
  13. function isNonEmptyString(str) {
  14. return typeof str === "string" && !!str.trim();
  15. }
  16. function parseString(setCookieValue, options) {
  17. var parts = setCookieValue.split(";").filter(isNonEmptyString);
  18. var nameValuePairStr = parts.shift();
  19. var parsed = parseNameValuePair(nameValuePairStr);
  20. var name = parsed.name;
  21. var value = parsed.value;
  22. options = options
  23. ? Object.assign({}, defaultParseOptions, options)
  24. : defaultParseOptions;
  25. if (isForbiddenKey(name)) {
  26. return null;
  27. }
  28. try {
  29. value = options.decodeValues ? decodeURIComponent(value) : value; // decode cookie value
  30. } catch (e) {
  31. console.error(
  32. "set-cookie-parser: failed to decode cookie value. Set options.decodeValues=false to disable decoding.",
  33. e
  34. );
  35. }
  36. var cookie = createNullObj();
  37. cookie.name = name;
  38. cookie.value = value;
  39. parts.forEach(function (part) {
  40. var sides = part.split("=");
  41. var key = sides.shift().trimLeft().toLowerCase();
  42. if (isForbiddenKey(key)) {
  43. return;
  44. }
  45. var value = sides.join("=");
  46. if (key === "expires") {
  47. cookie.expires = new Date(value);
  48. } else if (key === "max-age") {
  49. var n = parseInt(value, 10);
  50. if (!Number.isNaN(n)) cookie.maxAge = n;
  51. } else if (key === "secure") {
  52. cookie.secure = true;
  53. } else if (key === "httponly") {
  54. cookie.httpOnly = true;
  55. } else if (key === "samesite") {
  56. cookie.sameSite = value;
  57. } else if (key === "partitioned") {
  58. cookie.partitioned = true;
  59. } else if (key) {
  60. cookie[key] = value;
  61. }
  62. });
  63. return cookie;
  64. }
  65. function parseNameValuePair(nameValuePairStr) {
  66. // Parses name-value-pair according to rfc6265bis draft
  67. var name = "";
  68. var value = "";
  69. var nameValueArr = nameValuePairStr.split("=");
  70. if (nameValueArr.length > 1) {
  71. name = nameValueArr.shift();
  72. value = nameValueArr.join("="); // everything after the first =, joined by a "=" if there was more than one part
  73. } else {
  74. value = nameValuePairStr;
  75. }
  76. return { name: name, value: value };
  77. }
  78. function parse(input, options) {
  79. options = options
  80. ? Object.assign({}, defaultParseOptions, options)
  81. : defaultParseOptions;
  82. if (!input) {
  83. if (!options.map) {
  84. return [];
  85. } else {
  86. return createNullObj();
  87. }
  88. }
  89. if (input.headers) {
  90. if (typeof input.headers.getSetCookie === "function") {
  91. // for fetch responses - they combine headers of the same type in the headers array,
  92. // but getSetCookie returns an uncombined array
  93. input = input.headers.getSetCookie();
  94. } else if (input.headers["set-cookie"]) {
  95. // fast-path for node.js (which automatically normalizes header names to lower-case)
  96. input = input.headers["set-cookie"];
  97. } else {
  98. // slow-path for other environments - see #25
  99. var sch =
  100. input.headers[
  101. Object.keys(input.headers).find(function (key) {
  102. return key.toLowerCase() === "set-cookie";
  103. })
  104. ];
  105. // warn if called on a request-like object with a cookie header rather than a set-cookie header - see #34, 36
  106. if (!sch && input.headers.cookie && !options.silent) {
  107. console.warn(
  108. "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning."
  109. );
  110. }
  111. input = sch;
  112. }
  113. }
  114. if (!Array.isArray(input)) {
  115. input = [input];
  116. }
  117. if (!options.map) {
  118. return input
  119. .filter(isNonEmptyString)
  120. .map(function (str) {
  121. return parseString(str, options);
  122. })
  123. .filter(Boolean);
  124. } else {
  125. var cookies = createNullObj();
  126. return input.filter(isNonEmptyString).reduce(function (cookies, str) {
  127. var cookie = parseString(str, options);
  128. if (cookie && !isForbiddenKey(cookie.name)) {
  129. cookies[cookie.name] = cookie;
  130. }
  131. return cookies;
  132. }, cookies);
  133. }
  134. }
  135. /*
  136. Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
  137. that are within a single set-cookie field-value, such as in the Expires portion.
  138. This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
  139. Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
  140. React Native's fetch does this for *every* header, including set-cookie.
  141. Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
  142. Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
  143. */
  144. function splitCookiesString(cookiesString) {
  145. if (Array.isArray(cookiesString)) {
  146. return cookiesString;
  147. }
  148. if (typeof cookiesString !== "string") {
  149. return [];
  150. }
  151. var cookiesStrings = [];
  152. var pos = 0;
  153. var start;
  154. var ch;
  155. var lastComma;
  156. var nextStart;
  157. var cookiesSeparatorFound;
  158. function skipWhitespace() {
  159. while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
  160. pos += 1;
  161. }
  162. return pos < cookiesString.length;
  163. }
  164. function notSpecialChar() {
  165. ch = cookiesString.charAt(pos);
  166. return ch !== "=" && ch !== ";" && ch !== ",";
  167. }
  168. while (pos < cookiesString.length) {
  169. start = pos;
  170. cookiesSeparatorFound = false;
  171. while (skipWhitespace()) {
  172. ch = cookiesString.charAt(pos);
  173. if (ch === ",") {
  174. // ',' is a cookie separator if we have later first '=', not ';' or ','
  175. lastComma = pos;
  176. pos += 1;
  177. skipWhitespace();
  178. nextStart = pos;
  179. while (pos < cookiesString.length && notSpecialChar()) {
  180. pos += 1;
  181. }
  182. // currently special character
  183. if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
  184. // we found cookies separator
  185. cookiesSeparatorFound = true;
  186. // pos is inside the next cookie, so back up and return it.
  187. pos = nextStart;
  188. cookiesStrings.push(cookiesString.substring(start, lastComma));
  189. start = pos;
  190. } else {
  191. // in param ',' or param separator ';',
  192. // we continue from that comma
  193. pos = lastComma + 1;
  194. }
  195. } else {
  196. pos += 1;
  197. }
  198. }
  199. if (!cookiesSeparatorFound || pos >= cookiesString.length) {
  200. cookiesStrings.push(cookiesString.substring(start, cookiesString.length));
  201. }
  202. }
  203. return cookiesStrings;
  204. }
  205. module.exports = parse;
  206. module.exports.parse = parse;
  207. module.exports.parseString = parseString;
  208. module.exports.splitCookiesString = splitCookiesString;