CorsHandler: Finished implementing a full CORS support
continuous-integration/appveyor/branch AppVeyor build succeeded Details

master
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 = {}) { export function CorsHandler(opts = {}) {
const options = { const options = {
allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH', allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH',
allowedOrigins: opts.allowedOrigins || [],
allowedHeaders: opts.allowedHeaders, allowedHeaders: opts.allowedHeaders,
openerPolicy: 'same-origin', credentials: opts.credentials || false,
resourcePolicy: 'same-origin', exposeHeaders: opts.exposeHeaders || '',
embedderPolicy: 'require-corp', maxAge: opts.maxAge || '',
} }
return function(ctx) { 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 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-Origin'] = origin
ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods
if (reqHeaders && options.allowedHeaders !== false) { if (options.credentials) {
ctx.headers['Access-Control-Allow-Headers'] = reqHeaders ctx.headers['Access-Control-Allow-Credentials'] = 'true'
} }
ctx.status = 204
} }
} }

View File

@ -34,90 +34,380 @@ t.describe('#CorsHandler()', function() {
let corsHandler let corsHandler
let ctx let ctx
t.beforeEach(function() {
ctx = createCtx()
})
t.test('should return a handler', function() { t.test('should return a handler', function() {
corsHandler = CorsHandler() corsHandler = CorsHandler()
assert.strictEqual(typeof(corsHandler), 'function') assert.strictEqual(typeof(corsHandler), 'function')
}) })
t.test('should set status and headers', function() { t.describe('OPTIONS', function() {
const assertOrigin = 'http://my.site.here' t.beforeEach(function() {
const assertRequestHeaders = 'asdf,foobar' ctx = createCtx()
ctx.method = 'OPTIONS'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
}) })
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']) t.test('should set status and headers', function() {
assert.notOk(ctx.headers['Access-Control-Allow-Methods']) const assertOrigin = 'http://my.site.here'
assert.notOk(ctx.headers['Access-Control-Allow-Headers']) 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.notOk(ctx.headers['Vary'])
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.strictEqual(ctx.status, 204) 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() { t.describe('GET/POST/DELETE/PATCH/PUT', function() {
const assertOrigin = 'http://my.site.here' let testMethods = ['GET', 'POST', 'DELETE', 'PATCH', 'PUT']
const assertAllowedMethods = 'GET,HEAD'
const assertAllowedHeaders = 'test1,test2'
corsHandler = CorsHandler({ t.test('should set header but no status', function() {
allowedOrigins: [assertOrigin], testMethods.forEach(function(method) {
allowedMethods: assertAllowedMethods, ctx = createCtx()
allowedHeaders: assertAllowedHeaders, 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']) t.test('should set credential header if specifed', function() {
assert.notOk(ctx.headers['Access-Control-Allow-Methods']) testMethods.forEach(function(method) {
assert.notOk(ctx.headers['Access-Control-Allow-Headers']) 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) corsHandler = CorsHandler({
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) allowedOrigins: [assertOrigin],
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertAllowedHeaders) credentials: true,
assert.strictEqual(ctx.status, 204) })
}) ctx.req.headers['origin'] = assertOrigin
t.test('should not set any allowed headers if allowedHeaders is explicitly false', function() { assert.notOk(ctx.headers['Vary'])
const assertOrigin = 'http://my.site.here' assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
const assertAllowedMethods = 'GET,HEAD' assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler = CorsHandler({ corsHandler(ctx)
allowedOrigins: [assertOrigin],
allowedMethods: assertAllowedMethods, assert.strictEqual(ctx.headers['Vary'], 'Origin')
allowedHeaders: false, 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']) t.test('should set expose headers if specifed', function() {
assert.notOk(ctx.headers['Access-Control-Allow-Methods']) testMethods.forEach(function(method) {
assert.notOk(ctx.headers['Access-Control-Allow-Headers']) 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) corsHandler = CorsHandler({
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) allowedOrigins: [assertOrigin],
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], undefined) exposeHeaders: assertExposeHeaders,
assert.strictEqual(ctx.status, 204) })
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)
})
})
}) })
}) })