Skip to content

Commit

Permalink
feat(cors): added cors framework
Browse files Browse the repository at this point in the history
  • Loading branch information
H4ad committed Nov 19, 2022
1 parent ea6ca68 commit 8bf3425
Show file tree
Hide file tree
Showing 5 changed files with 500 additions and 0 deletions.
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@trpc/server": "^9.26.2",
"@types/aws-lambda": "^8.10.92",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/hapi": "^18.0.7",
"@types/jest": "^28.1.6",
Expand All @@ -98,6 +99,7 @@
"body-parser": "^1.19.1",
"codecov": "^3.8.1",
"commitizen": "^4.2.4",
"cors": "^2.8.5",
"cz-conventional-changelog": "^3.3.0",
"ejs": "^3.1.6",
"eslint": "^8.9.0",
Expand Down Expand Up @@ -131,9 +133,11 @@
"@hapi/hapi": ">= 20.0.0",
"@trpc/server": ">= 9.0.0",
"@types/aws-lambda": ">= 8.10.92",
"@types/cors": ">= 2.8.12",
"@types/express": ">= 4.15.4",
"@types/hapi": ">= 18.0.7",
"@types/koa": ">= 2.11.2",
"cors": ">= 2.8.5",
"express": ">= 4.15.4",
"fastify": ">= 3.0.0",
"fastify-3": "npm:[email protected]",
Expand Down
203 changes: 203 additions & 0 deletions src/frameworks/cors/cors.framework.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//#region Imports

import { IncomingMessage, ServerResponse } from 'http';
import cors, { CorsOptions } from 'cors';
import { FrameworkContract } from '../../contracts';
import { getDefaultIfUndefined } from '../../core';

//#endregion

/**
* The options to customize {@link CorsFramework}
*
* @breadcrumb Frameworks / CorsFramework
* @public
*/
export type CorsFrameworkOptions = CorsOptions & {
/**
* Send error 403 when cors is invalid. From what I read in `cors`, `fastify/cors` and [this problem](https://stackoverflow.com/questions/57212248/why-is-http-request-been-processed-in-action-even-when-cors-is-not-enabled)
* it is normal to process the request even if the origin is invalid.
* So this option will respond with error if this method was called from an invalid origin (or not allowed method) like [access control lib](https://github.com/primus/access-control/blob/master/index.js#L95-L115) .
*
* @defaultValue true
*/
forbiddenOnInvalidOriginOrMethod?: boolean;
};

/**
* The framework that handles cors for your api without relying on internals of the framework
*
* @example
* ```typescript
* import express from 'express';
* import { ServerlessAdapter } from '@h4ad/serverless-adapter';
* import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express';
* import { CorsFramework } from '@h4ad/serverless-adapter/lib/frameworks/cors';
*
* const expressFramework = new ExpressFramework();
* const options: CorsOptions = {}; // customize the options
* const framework = new CorsFramework(expressFramework, options);
*
* export const handler = ServerlessAdapter.new(null)
* .setFramework(framework)
* // set other configurations and then build
* .build();
* ```
*
* @breadcrumb Frameworks / CorsFramework
* @public
*/
export class CorsFramework<TApp> implements FrameworkContract<TApp> {
//#region Constructor

/**
* Default Constructor
*/
constructor(
protected readonly framework: FrameworkContract<TApp>,
protected readonly options?: CorsFrameworkOptions,
) {
this.cachedCorsInstance = cors(this.options);
}

//#endregion

/**
* All cors headers that can be added by cors package
*/
protected readonly corsHeaders: string[] = [
'Access-Control-Max-Age',
'Access-Control-Expose-Headers',
'Access-Control-Allow-Headers',
'Access-Control-Request-Headers',
'Access-Control-Allow-Credentials',
'Access-Control-Allow-Methods',
'Access-Control-Allow-Origin',
];

/**
* The cached instance of cors
*/
protected readonly cachedCorsInstance: ReturnType<typeof cors>;

//#region Public Methods

/**
* {@inheritDoc}
*/
public sendRequest(
app: TApp,
request: IncomingMessage,
response: ServerResponse,
): void {
this.cachedCorsInstance(
request,
response,
this.onCorsNext(app, request, response),
);
}

//#endregion

//#region Protected Methods

/**
* Handle next execution called by the cors package
*/
protected onCorsNext(
app: TApp,
request: IncomingMessage,
response: ServerResponse,
): () => void {
return () => {
this.formatHeaderValuesAddedByCorsPackage(response);

const errorOnInvalidOrigin = getDefaultIfUndefined(
this.options?.forbiddenOnInvalidOriginOrMethod,
true,
);

if (errorOnInvalidOrigin) {
const allowedOrigin = response.getHeader('access-control-allow-origin');
const isInvalidOrigin = this.isInvalidOriginOrMethodIsNotAllowed(
request,
allowedOrigin,
);

if (isInvalidOrigin) {
response.statusCode = 403;
response.setHeader('Content-Type', 'text/plain');
response.end(
[
'Invalid HTTP Access Control (CORS) request:',
` Origin: ${request.headers.origin}`,
` Method: ${request.method}`,
].join('\n'),
);

return;
}
}

this.framework.sendRequest(app, request, response);
};
}

/**
* Format the headers to be standardized with the rest of the library, such as ApiGatewayV2.
* Also, some frameworks don't support headers as an array, so we need to format the values.
*/
protected formatHeaderValuesAddedByCorsPackage(
response: ServerResponse,
): void {
for (const corsHeader of this.corsHeaders) {
const value = response.getHeader(corsHeader);

if (value === undefined) continue;

response.removeHeader(corsHeader);
response.setHeader(
corsHeader.toLowerCase(),
Array.isArray(value) ? value.join(',') : value,
);
}
}

/**
* Check if the origin is invalid or if the method is not allowed.
* Highly inspired by [access-control](https://github.com/primus/access-control/blob/master/index.js#L95-L115)
*/
protected isInvalidOriginOrMethodIsNotAllowed(
request: IncomingMessage,
allowedOrigin: number | string | string[] | undefined,
): boolean {
if (!allowedOrigin) return true;

if (
!!request.headers.origin &&
allowedOrigin !== '*' &&
request.headers.origin !== allowedOrigin
)
return true;

const notPermitedInMethods =
this.options &&
Array.isArray(this.options.methods) &&
this.options.methods.every(
m => m.toLowerCase() !== request.method?.toLowerCase(),
);
const differentMethod =
this.options &&
typeof this.options.methods === 'string' &&
this.options.methods
.split(',')
.every(m => m.trim().toLowerCase() !== request.method?.toLowerCase());

if (this.options?.methods && (notPermitedInMethods || differentMethod))
return true;

return false;
}

//#endregion
}
1 change: 1 addition & 0 deletions src/frameworks/cors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cors.framework';
Loading

0 comments on commit 8bf3425

Please sign in to comment.