CorsHandler: Finished implementing a full CORS support
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded

This commit is contained in:
Jonatan Nilsson 2022-03-24 15:50:31 +00:00
parent 7b682e8e95
commit acb099868b
2 changed files with 397 additions and 70 deletions

View file

@ -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
}
}

View file

@ -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)
})
})
})
})