set-cookie.js 6.5 KB

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