UNPKG

roads

Version:

An isomophic http framework

221 lines (217 loc) 12 kB
/** * cors.ts * Copyright(c) 2021 Aaron Hedges <aaron@dashron.com> * MIT Licensed * * This exposes a function that helps you manage CORS in your roads service */ import Response from '../core/response.js'; function getSingleHeader(headers, key) { if (headers) { // This is a little weirder than I would like, but it works better with typescript const val = headers[key]; if (Array.isArray(val)) { return val[0]; } return val; } } /** * Sets up everything you need for your server to properly respond to CORS requests. * * @param {object} [options] - A collection of different cors settings. * @param {object} [options.validOrigins] - An array of origin urls that can send requests to this API * @param {object} [options.supportsCredentials] - A boolean, true if you want this endpoint to receive cookies * @param {object} [options.responseHeaders] - An array of valid HTTP response headers * @param {object} [options.requestHeaders] - An array of valid HTTP request headers * @param {object} [options.validMethods] - An array of valid HTTP methods * @param {object} [options.cacheMaxAge] - The maximum age to cache the cors information * * @return {function} The middleware to bind to your road */ export function build(options) { const validOrigins = options.validOrigins || []; const supportsCredentials = options.supportsCredentials || false; const responseHeaders = options.responseHeaders || []; const requestHeaders = options.requestHeaders || []; const validMethods = options.validMethods || []; const cacheMaxAge = options.cacheMaxAge || null; const logger = options.logger || { log: () => { } }; /* Note: the comments below are pulled from the spec https://www.w3.org/TR/cors/ to help development */ const corsMiddleware = function (method, url, body, headers, next) { const corsResponseHeaders = {}; /* * Terms * "list of origins" consisting of zero or more origins that are allowed access to the resource. * Note: This can include the origin of the resource itself though be aware that requests to * cross-origin resources can be redirected back to the resource. * "list of methods" consisting of zero or more methods that are supported by the resource. * "list of headers" consisting of zero or more header field names that are supported by the resource. * "list of exposed headers" consisting of zero or more header field names of headers other than * the simple response headers that the resource might use and can be exposed. * "supports credentials flag" that indicates whether the resource supports user credentials * in the request. It is true when the resource does and false otherwise. * "Simple Requests" If the Origin header is not present terminate this set of * steps. The request is outside the scope of this specification. https://www.w3.org/TR/cors/#resource-requests * "Actual Requests" https://www.w3.org/TR/cors/#resource-requests * "Preflight Requests": If the Origin header is not present terminate this set of steps. The request is \ * outside the scope of this specification. https://www.w3.org/TR/cors/#resource-preflight-requests */ if (!headers || !headers.origin) { return next(); } const preflight = method === 'OPTIONS' && headers['access-control-request-method']; /* Simple: If the value of the Origin header is not a case-sensitive match for any of the values in list of origins do not set any additional headers and terminate this set of steps. Note: Always matching is acceptable since the list of origins can be unbounded. */ /* Preflight: If the value of the Origin header is not a case-sensitive match for any of the values in list of origins do not set any additional headers and terminate this set of steps. Note: Always matching is acceptable since the list of origins can be unbounded. Note: The Origin header can only contain a single origin as the user agent will not follow redirects. Implementation Note: Resources that wish to enable themselves to be shared with multiple Origins but do not respond uniformly with "*" must in practice generate the Access-Control-Allow-Origin header dynamically in response to every request they wish to allow. As a consequence, authors of such resources should send a Vary: Origin HTTP header or provide other appropriate control directives to prevent caching of such responses, which may be inaccurate if re-used across-origins. */ const originHeader = getSingleHeader(headers, 'origin'); if (validOrigins[0] !== '*' && originHeader && validOrigins.indexOf(originHeader) === -1) { logger.log('CORS ERROR: bad origin', originHeader); return next(); } if (preflight) { /* * Preflight * Let method be the value as result of parsing the Access-Control-Request-Method header. * If there is no Access-Control-Request-Method header or if parsing failed, do not set any additional headers * and terminate this set of steps. The request is outside the scope of this specification. */ const corsMethod = getSingleHeader(headers, 'access-control-request-method'); /* preflight If method is not a case-sensitive match for any of the values in list of methods do not set any additional headers and terminate this set of steps. Note: Always matching is acceptable since the list of methods can be unbounded. */ if (corsMethod && validMethods.findIndex((value) => { return value.toLowerCase() === corsMethod.toLowerCase(); })) { logger.log('CORS Error: bad method', corsMethod); return next(); } /* * preflight * Let header field-names be the values as result of parsing the Access-Control-Request-Headers headers. * * Note: If there are no Access-Control-Request-Headers headers let header field-names be the empty list. * Note: If parsing failed do not set any additional headers and terminate this set of steps. The request * is outside the scope of this specification. */ let headerNames = undefined; const acRequestHeaders = getSingleHeader(headers, 'access-control-request-headers'); try { headerNames = acRequestHeaders ? acRequestHeaders.split(',') : []; } catch (e) { logger.log('CORS Error: request headers parse fail'); return next(); } /* preflight If any of the header field-names is not a ASCII case-insensitive match for any of the values in list of headers do not set any additional headers and terminate this set of steps. Note: Always matching is acceptable since the list of headers can be unbounded. */ for (let i = 0; i < headerNames.length; i++) { if (requestHeaders.indexOf(headerNames[i]) === -1) { logger.log('CORS Error: invalid header requested', headerNames[i]); return next(); } } /* * Preflight * Optionally add a single Access-Control-Max-Age header with as value the amount of seconds the user agent is * allowed to cache the result of the request. */ if (typeof (cacheMaxAge) === 'number') { corsResponseHeaders['access-control-max-age'] = String(cacheMaxAge); } /* * Preflight * If method is a simple method this step may be skipped. * Add one or more Access-Control-Allow-Methods headers consisting of (a subset of) the list of methods. * Note: If a method is a simple method it does not need to be listed, but this is not prohibited. * Note: Since the list of methods can be unbounded, simply returning the method indicated by * Access-Control-Request-Method (if supported) can be enough. */ corsResponseHeaders['access-control-allow-methods'] = validMethods.join(', '); /* * Preflight * If each of the header field-names is a simple header and none is Content-Type, this step may be skipped. * Add one or more Access-Control-Allow-Headers headers consisting of (a subset of) the list of headers. * Note: If a header field name is a simple header and is not Content-Type, it is not required to be listed. * Content-Type is to be listed as only a subset of its values makes it qualify as simple header. * Note: Since the list of headers can be unbounded, simply returning supported headers from * Access-Control-Allow-Headers can be enough. */ corsResponseHeaders['access-control-allow-headers'] = requestHeaders.join(', '); } else { /* * Simple * If the list of exposed headers is not empty add one or more Access-Control-Expose-Headers headers, * with as values the header field names given in the list of exposed headers. * * By not adding the appropriate headers resource can also clear the preflight result cache of all entries * where origin is a case-sensitive match for the value of the Origin header and url is a case-sensitive * match for the URL of the resource. */ if (responseHeaders && responseHeaders.length) { corsResponseHeaders['access-control-expose-headers'] = responseHeaders.join(', '); } } /* * preflight * If the resource supports credentials add a single Access-Control-Allow-Origin header, * with the value of the Origin header as value, and add a single Access-Control-Allow-Credentials * header with the case-sensitive string "true" as value. * * Note: Otherwise, add a single Access-Control-Allow-Origin header, with either the value of the Origin header or * the string "*" as value. * Note: The string "*" cannot be used for a resource that supports credentials. */ /* * Simple * If the resource supports credentials add a single Access-Control-Allow-Origin header, * with the value of the Origin header as value, and add a single Access-Control-Allow-Credentials * header with the case-sensitive string "true" as value * * Note: Otherwise, add a single Access-Control-Allow-Origin header, with either the value of the Origin header or * the string "*" as value. * Note: The string "*" cannot be used for a resource that supports credentials. */ if (originHeader) { corsResponseHeaders['access-control-allow-origin'] = originHeader; } if (supportsCredentials) { corsResponseHeaders['access-control-allow-credentials'] = 'true'; } if (preflight) { return Promise.resolve(new Response('', 200, corsResponseHeaders)); } return next() .then((response) => { for (const key in corsResponseHeaders) { response.headers[key] = corsResponseHeaders[key]; } return response; }); }; return corsMiddleware; } //# sourceMappingURL=cors.js.map