index.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. 'use strict';
  2. const vary = require('vary');
  3. /**
  4. * CORS middleware
  5. *
  6. * @param {Object} [options]
  7. * - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
  8. * - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
  9. * - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
  10. * - {String|Array} allowHeaders `Access-Control-Allow-Headers`
  11. * - {String|Number} maxAge `Access-Control-Max-Age` in seconds
  12. * - {Boolean|Function(ctx)} credentials `Access-Control-Allow-Credentials`
  13. * - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
  14. * - {Boolean} secureContext `Cross-Origin-Opener-Policy` & `Cross-Origin-Embedder-Policy` headers.', default is false
  15. * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/Planned_changes
  16. * - {Boolean} privateNetworkAccess handle `Access-Control-Request-Private-Network` request by return `Access-Control-Allow-Private-Network`, default to false
  17. * @see https://wicg.github.io/private-network-access/
  18. * @return {Function} cors middleware
  19. * @public
  20. */
  21. module.exports = function(options) {
  22. const defaults = {
  23. allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
  24. secureContext: false,
  25. };
  26. options = {
  27. ...defaults,
  28. ...options,
  29. };
  30. if (Array.isArray(options.exposeHeaders)) {
  31. options.exposeHeaders = options.exposeHeaders.join(',');
  32. }
  33. if (Array.isArray(options.allowMethods)) {
  34. options.allowMethods = options.allowMethods.join(',');
  35. }
  36. if (Array.isArray(options.allowHeaders)) {
  37. options.allowHeaders = options.allowHeaders.join(',');
  38. }
  39. if (options.maxAge) {
  40. options.maxAge = String(options.maxAge);
  41. }
  42. options.keepHeadersOnError = options.keepHeadersOnError === undefined || !!options.keepHeadersOnError;
  43. return async function cors(ctx, next) {
  44. // If the Origin header is not present terminate this set of steps.
  45. // The request is outside the scope of this specification.
  46. const requestOrigin = ctx.get('Origin');
  47. // Always set Vary header
  48. // https://github.com/rs/cors/issues/10
  49. ctx.vary('Origin');
  50. if (!requestOrigin) return await next();
  51. let origin;
  52. if (typeof options.origin === 'function') {
  53. origin = await options.origin(ctx);
  54. if (!origin) return await next();
  55. } else {
  56. origin = options.origin || requestOrigin;
  57. }
  58. let credentials;
  59. if (typeof options.credentials === 'function') {
  60. credentials = await options.credentials(ctx);
  61. } else {
  62. credentials = !!options.credentials;
  63. }
  64. if (credentials && origin === '*') {
  65. origin = requestOrigin;
  66. }
  67. const headersSet = {};
  68. function set(key, value) {
  69. ctx.set(key, value);
  70. headersSet[key] = value;
  71. }
  72. if (ctx.method !== 'OPTIONS') {
  73. // Simple Cross-Origin Request, Actual Request, and Redirects
  74. set('Access-Control-Allow-Origin', origin);
  75. if (credentials === true) {
  76. set('Access-Control-Allow-Credentials', 'true');
  77. }
  78. if (options.exposeHeaders) {
  79. set('Access-Control-Expose-Headers', options.exposeHeaders);
  80. }
  81. if (options.secureContext) {
  82. set('Cross-Origin-Opener-Policy', 'same-origin');
  83. set('Cross-Origin-Embedder-Policy', 'require-corp');
  84. }
  85. if (!options.keepHeadersOnError) {
  86. return await next();
  87. }
  88. try {
  89. return await next();
  90. } catch (err) {
  91. const errHeadersSet = err.headers || {};
  92. const varyWithOrigin = vary.append(errHeadersSet.vary || errHeadersSet.Vary || '', 'Origin');
  93. delete errHeadersSet.Vary;
  94. err.headers = {
  95. ...errHeadersSet,
  96. ...headersSet,
  97. ...{ vary: varyWithOrigin },
  98. };
  99. throw err;
  100. }
  101. } else {
  102. // Preflight Request
  103. // If there is no Access-Control-Request-Method header or if parsing failed,
  104. // do not set any additional headers and terminate this set of steps.
  105. // The request is outside the scope of this specification.
  106. if (!ctx.get('Access-Control-Request-Method')) {
  107. // this not preflight request, ignore it
  108. return await next();
  109. }
  110. ctx.set('Access-Control-Allow-Origin', origin);
  111. if (credentials === true) {
  112. ctx.set('Access-Control-Allow-Credentials', 'true');
  113. }
  114. if (options.maxAge) {
  115. ctx.set('Access-Control-Max-Age', options.maxAge);
  116. }
  117. if (options.privateNetworkAccess && ctx.get('Access-Control-Request-Private-Network')) {
  118. ctx.set('Access-Control-Allow-Private-Network', 'true');
  119. }
  120. if (options.allowMethods) {
  121. ctx.set('Access-Control-Allow-Methods', options.allowMethods);
  122. }
  123. if (options.secureContext) {
  124. set('Cross-Origin-Opener-Policy', 'same-origin');
  125. set('Cross-Origin-Embedder-Policy', 'require-corp');
  126. }
  127. let allowHeaders = options.allowHeaders;
  128. if (!allowHeaders) {
  129. allowHeaders = ctx.get('Access-Control-Request-Headers');
  130. }
  131. if (allowHeaders) {
  132. ctx.set('Access-Control-Allow-Headers', allowHeaders);
  133. }
  134. ctx.status = 204;
  135. }
  136. };
  137. };