Skip to content
This repository was archived by the owner on Aug 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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