diff --git a/flaska.mjs b/flaska.mjs index a18e228..176b426 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -132,22 +132,59 @@ export function JsonHandler(org = {}) { export function CorsHandler(opts = {}) { const options = { allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH', + allowedOrigins: opts.allowedOrigins || [], allowedHeaders: opts.allowedHeaders, - openerPolicy: 'same-origin', - resourcePolicy: 'same-origin', - embedderPolicy: 'require-corp', + credentials: opts.credentials || false, + exposeHeaders: opts.exposeHeaders || '', + maxAge: opts.maxAge || '', } return function(ctx) { + // Always add vary header on origin. Prevent caches from + // accidentally caching wrong preflight request + ctx.headers['Vary'] = 'Origin' + + // Set status to 204 if OPTIONS. Just handy for flaska and + // other checking. + if (ctx.method === 'OPTIONS') { + ctx.status = 204 + } + + // Check origin is specified. Nothing needs to be done if + // there is no origin or it doesn't match let origin = ctx.req.headers['origin'] - let reqHeaders = options.allowedHeaders || ctx.req.headers['access-control-request-headers'] + if (!origin || !options.allowedOrigins.includes(origin)) { + return + } + + // Set some extra headers if this is a pre-flight. Most of + // these are not needed during a normal request. + if (ctx.method === 'OPTIONS') { + if (!ctx.req.headers['access-control-request-method']) { + return + } + + if (options.maxAge) { + ctx.headers['Access-Control-Max-Age'] = options.maxAge + } + + let reqHeaders = options.allowedHeaders + || ctx.req.headers['access-control-request-headers'] + if (reqHeaders && options.allowedHeaders !== false) { + ctx.headers['Access-Control-Allow-Headers'] = reqHeaders + } + ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods + } else { + if (options.exposeHeaders) { + ctx.headers['Access-Control-Expose-Headers'] = options.exposeHeaders + } + } ctx.headers['Access-Control-Allow-Origin'] = origin - ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods - if (reqHeaders && options.allowedHeaders !== false) { - ctx.headers['Access-Control-Allow-Headers'] = reqHeaders + + if (options.credentials) { + ctx.headers['Access-Control-Allow-Credentials'] = 'true' } - ctx.status = 204 } } diff --git a/test/middlewares.test.mjs b/test/middlewares.test.mjs index 6bbc022..c994efc 100644 --- a/test/middlewares.test.mjs +++ b/test/middlewares.test.mjs @@ -34,90 +34,380 @@ t.describe('#CorsHandler()', function() { let corsHandler let ctx - t.beforeEach(function() { - ctx = createCtx() - }) - t.test('should return a handler', function() { corsHandler = CorsHandler() assert.strictEqual(typeof(corsHandler), 'function') }) - t.test('should set status and headers', function() { - const assertOrigin = 'http://my.site.here' - const assertRequestHeaders = 'asdf,foobar' - - corsHandler = CorsHandler({ - allowedOrigins: [assertOrigin], + t.describe('OPTIONS', function() { + t.beforeEach(function() { + ctx = createCtx() + ctx.method = 'OPTIONS' }) - ctx.method = 'OPTIONS' - ctx.req.headers['origin'] = assertOrigin - ctx.req.headers['access-control-request-method'] = 'GET' - ctx.req.headers['access-control-request-headers'] = assertRequestHeaders - assert.notOk(ctx.headers['Access-Control-Allow-Origin']) - assert.notOk(ctx.headers['Access-Control-Allow-Methods']) - assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + t.test('should set status and headers', function() { + const assertOrigin = 'http://my.site.here' + const assertRequestHeaders = 'asdf,foobar' - corsHandler(ctx) + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = assertRequestHeaders - assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) - assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') - assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) - assert.strictEqual(ctx.status, 204) + assert.notOk(ctx.headers['Vary']) + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) + assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) + assert.notOk(ctx.headers['Access-Control-Max-Age']) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should set Allow-Credentials if credentials is specified', function() { + const assertOrigin = 'http://my.site.here' + const assertRequestHeaders = 'asdf,foobar' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + credentials: true, + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = assertRequestHeaders + + assert.notOk(ctx.headers['Vary']) + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.strictEqual(ctx.headers['Access-Control-Allow-Credentials'], 'true') + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) + assert.notOk(ctx.headers['Access-Control-Max-Age']) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should set Max-Age if maxAge is specified', function() { + const assertOrigin = 'http://my.site.here' + const assertRequestHeaders = 'asdf,foobar' + const assertMaxAge = '600' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + maxAge: assertMaxAge, + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = assertRequestHeaders + + assert.notOk(ctx.headers['Vary']) + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Max-Age'], assertMaxAge) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) + assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should support custom allowed methods and headers', function() { + const assertOrigin = 'http://my.site.here' + const assertAllowedMethods = 'GET,HEAD' + const assertAllowedHeaders = 'test1,test2' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + allowedMethods: assertAllowedMethods, + allowedHeaders: assertAllowedHeaders, + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertAllowedHeaders) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should not set any allowed headers if allowedHeaders is explicitly false', function() { + const assertOrigin = 'http://my.site.here' + const assertAllowedMethods = 'GET,HEAD' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + allowedMethods: assertAllowedMethods, + allowedHeaders: false, + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], undefined) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should not add any headers if origin missing', function() { + corsHandler = CorsHandler({ + allowedOrigins: ['https://test.com'], + }) + ctx.req.headers['origin'] = null + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should not add any headers if origin not found', function() { + const assertOrigin = 'http://my.site.here' + const assertAllowedMethods = 'GET,HEAD' + + corsHandler = CorsHandler({ + allowedOrigins: ['https://my.site.here'], + allowedMethods: assertAllowedMethods, + allowedHeaders: false, + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should not add any headers if request-method is missing', function() { + const assertOrigin = 'http://my.site.here' + + corsHandler = CorsHandler({ + allowedOrigins: ['http://my.site.here'], + }) + ctx.req.headers['origin'] = assertOrigin + delete ctx.req.headers['access-control-request-method'] + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.strictEqual(ctx.status, 204) + }) }) - t.test('should support custom allowed methods and headers', function() { - const assertOrigin = 'http://my.site.here' - const assertAllowedMethods = 'GET,HEAD' - const assertAllowedHeaders = 'test1,test2' + t.describe('GET/POST/DELETE/PATCH/PUT', function() { + let testMethods = ['GET', 'POST', 'DELETE', 'PATCH', 'PUT'] - corsHandler = CorsHandler({ - allowedOrigins: [assertOrigin], - allowedMethods: assertAllowedMethods, - allowedHeaders: assertAllowedHeaders, + t.test('should set header but no status', function() { + testMethods.forEach(function(method) { + ctx = createCtx() + ctx.method = method + ctx.status = method + + const assertOrigin = 'http://my.site.here' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + }) + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Vary']) + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.notOk(ctx.headers['Access-Control-Expose-Headers']) + assert.strictEqual(ctx.status, method) + }) }) - ctx.method = 'OPTIONS' - ctx.req.headers['origin'] = assertOrigin - ctx.req.headers['access-control-request-method'] = 'GET' - ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' - assert.notOk(ctx.headers['Access-Control-Allow-Origin']) - assert.notOk(ctx.headers['Access-Control-Allow-Methods']) - assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + t.test('should set credential header if specifed', function() { + testMethods.forEach(function(method) { + ctx = createCtx() + ctx.method = method + ctx.status = method - corsHandler(ctx) + const assertOrigin = 'http://my.site.here' - assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) - assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) - assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertAllowedHeaders) - assert.strictEqual(ctx.status, 204) - }) + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + credentials: true, + }) + ctx.req.headers['origin'] = assertOrigin - t.test('should not set any allowed headers if allowedHeaders is explicitly false', function() { - const assertOrigin = 'http://my.site.here' - const assertAllowedMethods = 'GET,HEAD' + assert.notOk(ctx.headers['Vary']) + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) - corsHandler = CorsHandler({ - allowedOrigins: [assertOrigin], - allowedMethods: assertAllowedMethods, - allowedHeaders: false, + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.strictEqual(ctx.headers['Access-Control-Allow-Credentials'], 'true') + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.notOk(ctx.headers['Access-Control-Expose-Headers']) + assert.strictEqual(ctx.status, method) + }) }) - ctx.method = 'OPTIONS' - ctx.req.headers['origin'] = assertOrigin - ctx.req.headers['access-control-request-method'] = 'GET' - ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' - assert.notOk(ctx.headers['Access-Control-Allow-Origin']) - assert.notOk(ctx.headers['Access-Control-Allow-Methods']) - assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + t.test('should set expose headers if specifed', function() { + testMethods.forEach(function(method) { + const assertExposeHeaders = 'Some, Test, Here' + ctx = createCtx() + ctx.method = method + ctx.status = method - corsHandler(ctx) + const assertOrigin = 'http://my.site.here' - assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) - assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) - assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], undefined) - assert.strictEqual(ctx.status, 204) + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + exposeHeaders: assertExposeHeaders, + }) + ctx.req.headers['origin'] = assertOrigin + + assert.notOk(ctx.headers['Vary']) + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.strictEqual(ctx.headers['Access-Control-Expose-Headers'], assertExposeHeaders) + assert.strictEqual(ctx.status, method) + }) + }) + + t.test('should not add any headers if origin missing', function() { + testMethods.forEach(function(method) { + ctx = createCtx() + ctx.method = method + ctx.status = method + + corsHandler = CorsHandler({ + allowedOrigins: ['https://test.com'], + }) + ctx.req.headers['origin'] = null + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.notOk(ctx.headers['Access-Control-Expose-Headers']) + assert.strictEqual(ctx.status, method) + }) + }) + + t.test('should not add any headers if origin not found', function() { + testMethods.forEach(function(method) { + ctx = createCtx() + ctx.method = method + ctx.status = method + + const assertOrigin = 'http://my.site.here' + const assertAllowedMethods = 'GET,HEAD' + + corsHandler = CorsHandler({ + allowedOrigins: ['https://my.site.here'], + allowedMethods: assertAllowedMethods, + allowedHeaders: false, + }) + ctx.req.headers['origin'] = assertOrigin + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Vary'], 'Origin') + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + assert.notOk(ctx.headers['Access-Control-Expose-Headers']) + assert.strictEqual(ctx.status, method) + }) + }) }) })