corser.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /**
  2. * Specification: http://www.w3.org/TR/2012/WD-cors-20120403/
  3. * W3C Working Draft 3 April 2012
  4. */
  5. "use strict";
  6. /*jshint node:true */
  7. var simpleMethods, simpleRequestHeaders, simpleResponseHeaders, toLowerCase, checkOriginMatch;
  8. // A method is said to be a simple method if it is a case-sensitive match for one of the following:
  9. Object.defineProperty(exports, "simpleMethods", {
  10. get: function () {
  11. return [
  12. "GET",
  13. "HEAD",
  14. "POST"
  15. ];
  16. }
  17. });
  18. simpleMethods = exports.simpleMethods;
  19. // A header is said to be a simple header if the header field name is an ASCII case-insensitive match for one of
  20. // the following:
  21. Object.defineProperty(exports, "simpleRequestHeaders", {
  22. get: function () {
  23. return [
  24. "accept",
  25. "accept-language",
  26. "content-language",
  27. "content-type"
  28. ];
  29. }
  30. });
  31. simpleRequestHeaders = exports.simpleRequestHeaders;
  32. // A header is said to be a simple response header if the header field name is an ASCII case-insensitive
  33. // match for one of the following:
  34. Object.defineProperty(exports, "simpleResponseHeaders", {
  35. get: function () {
  36. return [
  37. "cache-control",
  38. "content-language",
  39. "content-type",
  40. "expires",
  41. "last-modified",
  42. "pragma"
  43. ];
  44. }
  45. });
  46. simpleResponseHeaders = exports.simpleResponseHeaders;
  47. toLowerCase = function (array) {
  48. return array.map(function (el) {
  49. return el.toLowerCase();
  50. });
  51. };
  52. checkOriginMatch = function (originHeader, origins, callback) {
  53. if (typeof origins === "function") {
  54. origins(originHeader, function (err, allow) {
  55. callback(err, allow);
  56. });
  57. } else if (origins.length > 0) {
  58. callback(null, origins.some(function (origin) {
  59. return origin === originHeader;
  60. }));
  61. } else {
  62. // Always matching is acceptable since the list of origins can be unbounded.
  63. callback(null, true);
  64. }
  65. };
  66. exports.create = function (options) {
  67. options = options || {};
  68. options.origins = options.origins || [];
  69. options.methods = options.methods || simpleMethods;
  70. if (options.hasOwnProperty("requestHeaders") === true) {
  71. options.requestHeaders = toLowerCase(options.requestHeaders);
  72. } else {
  73. options.requestHeaders = simpleRequestHeaders;
  74. }
  75. if (options.hasOwnProperty("responseHeaders") === true) {
  76. options.responseHeaders = toLowerCase(options.responseHeaders);
  77. } else {
  78. options.responseHeaders = simpleResponseHeaders;
  79. }
  80. options.maxAge = options.maxAge || null;
  81. options.supportsCredentials = options.supportsCredentials || false;
  82. if (options.hasOwnProperty("endPreflightRequests") === false) {
  83. options.endPreflightRequests = true;
  84. }
  85. return function (req, res, next) {
  86. var methodMatches, headersMatch, requestMethod, requestHeaders, exposedHeaders, endPreflight;
  87. // If the Origin header is not present terminate this set of steps.
  88. if (!req.headers.hasOwnProperty("origin")) {
  89. // The request is outside the scope of the CORS specification. If there is no Origin header,
  90. // it could be a same-origin request. Let's let the user-agent handle this situation.
  91. next();
  92. } else {
  93. // If the value of the Origin header is not a case-sensitive match for any of the values in
  94. // list of origins, do not set any additional headers and terminate this set of steps.
  95. checkOriginMatch(req.headers.origin, options.origins, function (err, originMatches) {
  96. if (err !== null) {
  97. next(err);
  98. } else {
  99. if (typeof originMatches !== "boolean" || originMatches === false) {
  100. next();
  101. } else {
  102. // Respond to preflight request.
  103. if (req.method === "OPTIONS") {
  104. endPreflight = function () {
  105. if (options.endPreflightRequests === true) {
  106. res.writeHead(204);
  107. res.end();
  108. } else {
  109. next();
  110. }
  111. };
  112. // If there is no Access-Control-Request-Method header or if parsing failed, do not set
  113. // any additional headers and terminate this set of steps.
  114. if (!req.headers.hasOwnProperty("access-control-request-method")) {
  115. endPreflight();
  116. } else {
  117. requestMethod = req.headers["access-control-request-method"];
  118. // If there are no Access-Control-Request-Headers headers let header field-names be the
  119. // empty list. If parsing failed do not set any additional headers and terminate this set
  120. // of steps.
  121. // Checking for an empty header is a workaround for a bug Chrome 52:
  122. // https://bugs.chromium.org/p/chromium/issues/detail?id=633729
  123. if (req.headers.hasOwnProperty("access-control-request-headers") && req.headers["access-control-request-headers"] !== "") {
  124. requestHeaders = toLowerCase(req.headers["access-control-request-headers"].split(/,\s*/));
  125. } else {
  126. requestHeaders = [];
  127. }
  128. // If method is not a case-sensitive match for any of the values in list of methods do not
  129. // set any additional headers and terminate this set of steps.
  130. methodMatches = options.methods.indexOf(requestMethod) !== -1;
  131. if (methodMatches === false) {
  132. endPreflight();
  133. } else {
  134. // If any of the header field-names is not a ASCII case-insensitive match for any of
  135. // the values in list of headers do not set any additional headers and terminate this
  136. // set of steps.
  137. headersMatch = requestHeaders.every(function (requestHeader) {
  138. // Browsers automatically add Origin to Access-Control-Request-Headers. However,
  139. // Origin is not one of the simple request headers. Therefore, the header is
  140. // accepted even if it is not in the list of request headers because CORS would
  141. // not work without it.
  142. if (requestHeader === "origin") {
  143. return true;
  144. } else {
  145. if (options.requestHeaders.indexOf(requestHeader) !== -1) {
  146. return true;
  147. } else {
  148. return false;
  149. }
  150. }
  151. });
  152. if (headersMatch === false) {
  153. endPreflight();
  154. } else {
  155. if (options.supportsCredentials === true) {
  156. // If the resource supports credentials add a single Access-Control-Allow-Origin
  157. // header, with the value of the Origin header as value, and add a single
  158. // Access-Control-Allow-Credentials header with the literal string "true"
  159. // as value.
  160. res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  161. res.setHeader("Access-Control-Allow-Credentials", "true");
  162. } else {
  163. // Otherwise, add a single Access-Control-Allow-Origin header, with either the
  164. // value of the Origin header or the string "*" as value.
  165. if (options.origins.length > 0 || typeof options.origins === "function") {
  166. res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  167. } else {
  168. res.setHeader("Access-Control-Allow-Origin", "*");
  169. }
  170. }
  171. // Optionally add a single Access-Control-Max-Age header with as value the amount
  172. // of seconds the user agent is allowed to cache the result of the request.
  173. if (options.maxAge !== null) {
  174. res.setHeader("Access-Control-Max-Age", options.maxAge);
  175. }
  176. // Add one or more Access-Control-Allow-Methods headers consisting of (a subset
  177. // of) the list of methods.
  178. res.setHeader("Access-Control-Allow-Methods", options.methods.join(","));
  179. // Add one or more Access-Control-Allow-Headers headers consisting of (a subset
  180. // of) the list of headers.
  181. res.setHeader("Access-Control-Allow-Headers", options.requestHeaders.join(","));
  182. // And out.
  183. endPreflight();
  184. }
  185. }
  186. }
  187. } else {
  188. if (options.supportsCredentials === true) {
  189. // If the resource supports credentials add a single Access-Control-Allow-Origin header,
  190. // with the value of the Origin header as value, and add a single
  191. // Access-Control-Allow-Credentials header with the literal string "true" as value.
  192. res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  193. res.setHeader("Access-Control-Allow-Credentials", "true");
  194. } else {
  195. // Otherwise, add a single Access-Control-Allow-Origin header, with either the value of
  196. // the Origin header or the literal string "*" as value.
  197. // If the list of origins is empty, use "*" as value.
  198. if (options.origins.length > 0 || typeof options.origins === "function") {
  199. res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  200. } else {
  201. res.setHeader("Access-Control-Allow-Origin", "*");
  202. }
  203. }
  204. // If the list of exposed headers is not empty add one or more Access-Control-Expose-Headers
  205. // headers, with as values the header field names given in the list of exposed headers.
  206. exposedHeaders = options.responseHeaders.filter(function (optionsResponseHeader) {
  207. return simpleResponseHeaders.indexOf(optionsResponseHeader) === -1;
  208. });
  209. if (exposedHeaders.length > 0) {
  210. res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(","));
  211. }
  212. // And out.
  213. next();
  214. }
  215. }
  216. }
  217. });
  218. }
  219. };
  220. };