From 8dec921c10363d74ad1c0655b46824d74484be8f Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sun, 29 Nov 2020 23:58:42 +0100 Subject: [PATCH] feat: Make CorsHandler customizable. --- config/presets/middleware.json | 11 +++++++- src/server/middleware/CorsHandler.ts | 24 +++++++++++++---- .../server/middleware/CorsHandler.test.ts | 26 ++++++++++++++----- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/config/presets/middleware.json b/config/presets/middleware.json index 34efcd5ff..3f77595fc 100644 --- a/config/presets/middleware.json +++ b/config/presets/middleware.json @@ -6,7 +6,16 @@ "@type": "AllVoidCompositeHandler", "AllVoidCompositeHandler:_handlers": [ { - "@type": "CorsHandler" + "@type": "CorsHandler", + "CorsHandler:_options_methods": [ + "GET", + "HEAD", + "OPTIONS", + "POST", + "PUT", + "PATCH", + "DELETE" + ] }, { "@type": "HeaderHandler", diff --git a/src/server/middleware/CorsHandler.ts b/src/server/middleware/CorsHandler.ts index 0a502d8c6..f16a40a1d 100644 --- a/src/server/middleware/CorsHandler.ts +++ b/src/server/middleware/CorsHandler.ts @@ -1,21 +1,35 @@ import cors from 'cors'; +import type { CorsOptions } from 'cors'; import type { RequestHandler } from 'express'; import { HttpHandler } from '../HttpHandler'; import type { HttpRequest } from '../HttpRequest'; import type { HttpResponse } from '../HttpResponse'; +const defaultOptions: CorsOptions = { + origin: (origin: any, callback: any): void => callback(null, origin ?? '*'), +}; + +// Components.js does not support the full CorsOptions yet +interface SimpleCorsOptions { + origin?: string; + methods?: string[]; + allowedHeaders?: string[]; + exposedHeaders?: string[]; + credentials?: boolean; + maxAge?: number; + preflightContinue?: boolean; + optionsSuccessStatus?: number; +} + /** * Handler that sets CORS options on the response. */ export class CorsHandler extends HttpHandler { private readonly corsHandler: RequestHandler; - public constructor() { + public constructor(options: SimpleCorsOptions = {}) { super(); - this.corsHandler = cors({ - origin: (origin, callback): void => callback(null, (origin ?? '*') as any), - methods: [ 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE' ], - }); + this.corsHandler = cors({ ...defaultOptions, ...options }); } public async handle(input: { request: HttpRequest; response: HttpResponse }): Promise { diff --git a/test/unit/server/middleware/CorsHandler.test.ts b/test/unit/server/middleware/CorsHandler.test.ts index aec83c983..bbe168fb2 100644 --- a/test/unit/server/middleware/CorsHandler.test.ts +++ b/test/unit/server/middleware/CorsHandler.test.ts @@ -3,22 +3,21 @@ import { CorsHandler } from '../../../../src/server/middleware/CorsHandler'; import { guardStream } from '../../../../src/util/GuardedStream'; describe('a CorsHandler', (): void => { - let handler: CorsHandler; + it('sets regular CORS headers.', async(): Promise => { + const handler = new CorsHandler(); - beforeAll(async(): Promise => { - handler = new CorsHandler(); - }); - - it('returns CORS headers.', async(): Promise => { const request = guardStream(createRequest()); const response = createResponse(); await handler.handleSafe({ request, response }); + expect(response.getHeaders()).toEqual(expect.objectContaining({ 'access-control-allow-origin': '*', })); }); it('echoes the origin when specified.', async(): Promise => { + const handler = new CorsHandler(); + const request = guardStream(createRequest({ headers: { origin: 'example.org', @@ -26,8 +25,23 @@ describe('a CorsHandler', (): void => { })); const response = createResponse(); await handler.handleSafe({ request, response }); + expect(response.getHeaders()).toEqual(expect.objectContaining({ 'access-control-allow-origin': 'example.org', })); }); + + it('supports customizations.', async(): Promise => { + const handler = new CorsHandler({ + exposedHeaders: [ 'Custom-Header' ], + }); + + const request = guardStream(createRequest()); + const response = createResponse(); + await handler.handleSafe({ request, response }); + + expect(response.getHeaders()).toEqual(expect.objectContaining({ + 'access-control-expose-headers': 'Custom-Header', + })); + }); });