Skip to content
This repository has been archived by the owner on Aug 8, 2024. It is now read-only.

Commit

Permalink
Adds customizable CORS configurations (#52)
Browse files Browse the repository at this point in the history
* Add new CORS functionality, confirm original tests still work, update readme.

* Update readme and allow origin to be passed a function

* Add example to docs, as per code comment

* Closes #51
  • Loading branch information
TerryMooreII authored Sep 1, 2020
1 parent 1cd933e commit dcb03db
Show file tree
Hide file tree
Showing 4 changed files with 550 additions and 21 deletions.
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ See the following example:
<details>
<summary>CORS example</summary>
<summary>Default CORS example</summary>
```js
import * as router from 'aws-lambda-router'
Expand Down Expand Up @@ -176,6 +176,49 @@ If CORS is activated, these default headers will be sent on every response:
"Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,HEAD,PATCH'"
"Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
### Customizing CORS
To customize CORS for all routes pass any of the following options to the `proxyIntegration` `cors` property. If a property is not set then it will default to the above default CORS headers.
* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. Possible values:
- `Boolean` - set `origin` to `true` to reflect the request origin or set it to `false` to disable CORS.
- `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed.
- `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com".
- `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com".
- `Function` - set `origin` to a function to be evaluated. The function will get passed the `APIGatewayProxyEvent` and must return the allowed origin or `false`
* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted.
<details>
<summary>Customize CORS example</summary>
```js
import * as router from 'aws-lambda-router'

export const handler = router.handler({
// for handling an http-call from an AWS Apigateway proxyIntegration we provide the following config:
proxyIntegration: {
cors: {
origin: 'https://test.example.com', // Only allow CORS request from this url
methods: ['GET', 'POST', 'PUT'] // Only allow these HTTP methods to make requests
},
routes: [
{
path: '/graphql',
method: 'POST',
// provide a function to be called with the appropriate data
action: (request, context) => doAnything(request.body)
}
]
}
})
```
</details>
## Error mapping
Expand Down
197 changes: 197 additions & 0 deletions lib/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { APIGatewayProxyEvent } from 'aws-lambda'

type HeaderKeyValue = {
key: string,
value: any
}

type HeaderObject = Array<HeaderKeyValue>

type CorsOrigin = string | boolean | RegExp | Array<RegExp | string> | Function| undefined

export interface CorsOptions {
origin?: CorsOrigin
methods?: string | string[]
allowedHeaders?: string | string[]
exposedHeaders?: string | string[]
maxAge?: number
credentials?: boolean
}

const defaults = {
origin: '*',
methods: 'GET,POST,PUT,DELETE,HEAD,PATCH',
allowedHeaders: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
}

function isString(s: any) {
return typeof s === 'string' || s instanceof String
}

const isOriginAllowed = (origin: string, allowedOrigin: CorsOrigin): boolean => {
if (Array.isArray(allowedOrigin)) {
for (var i = 0; i < allowedOrigin.length; ++i) {
if (isOriginAllowed(origin, allowedOrigin[i])) {
return true
}
}
return false
} else if (isString(allowedOrigin)) {
return origin === allowedOrigin
} else if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin)
} else {
return !!allowedOrigin
}
}

const configureOrigin = (options: CorsOptions, event: APIGatewayProxyEvent): HeaderObject => {
const { origin } = options
const headers: HeaderObject = []

if (origin === true || origin === '*') {
headers.push({
key: 'Access-Control-Allow-Origin',
value: '*'
})
} else if(isString(origin)) {
headers.push({
key: 'Access-Control-Allow-Origin',
value: origin
}, {
key: 'Vary',
value: 'Origin'
})
} else if(typeof origin === 'function') {
headers.push({
key: 'Access-Control-Allow-Origin',
value: origin(event)
}, {
key: 'Vary',
value: 'Origin'
})
} else {
const requestOrigin: string = event.headers.origin
const isAllowed: boolean = isOriginAllowed(requestOrigin, origin)

headers.push({
key: 'Access-Control-Allow-Origin',
value: isAllowed ? requestOrigin : false
}, {
key: 'Vary',
value: 'Origin'
})
}

return headers
}

const configureMethods = (options: CorsOptions): HeaderObject => {
const { methods } = options

return [{
key: 'Access-Control-Allow-Methods',
value: Array.isArray(methods) ? methods.join(',') : methods
}]
}

const configureAllowedHeaders = (options: CorsOptions, event: APIGatewayProxyEvent): HeaderObject => {
let { allowedHeaders } = options
const headers = []

if (!allowedHeaders) {
allowedHeaders = event.headers['Access-Control-Request-Headers']
headers.push({
key: 'Vary',
value: 'Access-Control-Request-Headers'
})
} else if(Array.isArray(allowedHeaders)) {
allowedHeaders = allowedHeaders.join(',')
}

if(allowedHeaders && allowedHeaders.length) {
headers.push({
key: 'Access-Control-Allow-Headers',
value: allowedHeaders
})
}

return headers
}

const configureExposedHeaders = (options: CorsOptions): HeaderObject => {
let { exposedHeaders } = options

if (!exposedHeaders) {
return []
} else if(Array.isArray(exposedHeaders)){
exposedHeaders = exposedHeaders.join(',')
}
if (exposedHeaders) {
return [{
key: 'Access-Control-Expose-Headers',
value: exposedHeaders
}]
}
return []
}


const configureAllowMaxAge = (options: CorsOptions): HeaderObject => {
const { maxAge } = options

return !maxAge ? [] : [
{
key: 'Access-Control-Max-Age',
value: `${maxAge}`
}
]
}


const configureCredentials = (options: CorsOptions): HeaderObject => {
const { credentials } = options

return credentials === true
? [{
key: 'Access-Control-Allow-Credentials',
value: 'true'
}] : []
}

const generateHeaders = (headersArray: Array<HeaderObject> ) => {
const vary: string[] = []
const headers: any = {}

headersArray.forEach((header: HeaderObject) => {
header.forEach((h: HeaderKeyValue) => {
if (h.key === 'Vary' && h.value) {
vary.push(h.value)
} else {
headers[h.key] = h.value
}
})
})

return {
...headers,
...(vary.length && { 'Vary': vary.join(',') })
}
}

export const addCorsHeaders = (options: CorsOptions | boolean, event: APIGatewayProxyEvent) => {
if (options === false) {
return {}
}

const corsOptions = Object.assign({}, defaults, typeof options === 'object' ? options : {})

return generateHeaders([
configureOrigin(corsOptions, event),
configureExposedHeaders(corsOptions),
configureCredentials(corsOptions),
configureMethods(corsOptions),
configureAllowedHeaders(corsOptions, event),
configureAllowMaxAge(corsOptions)
])
}
32 changes: 12 additions & 20 deletions lib/proxyIntegration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'

import { ProcessMethod } from './EventProcessor'
import { addCorsHeaders, CorsOptions } from './cors';

type ProxyIntegrationParams = {
paths?: { [paramId: string]: string }
Expand Down Expand Up @@ -34,7 +35,7 @@ export type ProxyIntegrationError = {
}

export interface ProxyIntegrationConfig {
cors?: boolean
cors?: CorsOptions | boolean
routes: ProxyIntegrationRoute[]
debug?: boolean
errorMapping?: ProxyIntegrationErrorMapping
Expand All @@ -49,13 +50,6 @@ const NO_MATCHING_ACTION = (request: ProxyIntegrationEvent) => {
}
}

const addCorsHeaders = (toAdd: APIGatewayProxyResult['headers'] = {}) => {
toAdd['Access-Control-Allow-Origin'] = '*'
toAdd['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,HEAD,PATCH'
toAdd['Access-Control-Allow-Headers'] = 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
return toAdd
}

const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute, 'action'>, event: ProxyIntegrationEvent,
context: APIGatewayEventRequestContext, headers: APIGatewayProxyResult['headers']) => {

Expand Down Expand Up @@ -97,17 +91,15 @@ export const process: ProcessMethod<ProxyIntegrationConfig, APIGatewayProxyEvent
return null
}

const headers: APIGatewayProxyResult['headers'] = {}
if (proxyIntegrationConfig.cors) {
addCorsHeaders(headers)
if (event.httpMethod === 'OPTIONS') {
return Promise.resolve({
statusCode: 200,
headers,
body: ''
})
}
if (event.httpMethod === 'OPTIONS') {
return Promise.resolve({
statusCode: 200,
headers: proxyIntegrationConfig.cors ? addCorsHeaders(proxyIntegrationConfig.cors, event) : {},
body: ''
})
}

const headers: APIGatewayProxyResult['headers'] = proxyIntegrationConfig.cors ? addCorsHeaders(proxyIntegrationConfig.cors, event) : {};
Object.assign(headers, { 'Content-Type': 'application/json' }, proxyIntegrationConfig.defaultHeaders)

// assure necessary values have sane defaults:
Expand Down Expand Up @@ -188,14 +180,14 @@ const convertError = (error: ProxyIntegrationError | Error, errorMapping?: Proxy
return {
statusCode: error.statusCode,
body: JSON.stringify({ message: error.message, error: error.statusCode }),
headers: addCorsHeaders({})
headers: addCorsHeaders({}, {} as APIGatewayProxyEvent)
}
}
try {
return {
statusCode: 500,
body: JSON.stringify({ error: 'ServerError', message: `Generic error:${JSON.stringify(error)}` }),
headers: addCorsHeaders({})
headers: addCorsHeaders({}, {} as APIGatewayProxyEvent)
}
} catch (stringifyError) { }

Expand Down
Loading

0 comments on commit dcb03db

Please sign in to comment.