Skip to content

Commit

Permalink
fix: Replaced exact-object-key matching for API Gateway/ALB with matc…
Browse files Browse the repository at this point in the history
…hing against known-unique keys for each type. Also added event example fixtures from users.

Signed-off-by: mrickard <[email protected]>
  • Loading branch information
mrickard committed Nov 26, 2024
1 parent ef1a466 commit ebdfb27
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 50 deletions.
103 changes: 55 additions & 48 deletions lib/serverless/api-gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,72 +113,79 @@ function isLambdaProxyEvent(event) {
return isGatewayV1Event(event) || isGatewayV2Event(event) || isAlbEvent(event)
}

/**
* Iterates over the minimum signature properties of an event triggered by API Gateway
* to determine the version. V1 and V2 have a lot of overlap, but some signature differences.
*
* @param {object} targetEvent The event to inspect.
* @param {Array} searchFor An array of keys unique to this proxy type
* @returns {boolean} Whether this event has matches for the keys we're checking
*/
function findProperties(targetEvent, searchFor) {
const keys = Object.keys(targetEvent)
for (const el of searchFor) {
if (keys.indexOf(el) > -1) {
return true
}
}
return false
}

/**
* We need to determine whether this invocation event was triggered from a proxy: API Gateway V1, V2, ALB, or some other service.
* API Gateway V1/V2 and ALB events aren't guaranteed always to have the same properties in all cases. There are, however, properties
* that are vary consistently depending on the service triggering the event.
*
* API Gateway v2 HTTP
* API Gateway v1 HTTP: top-level httpMethod, resource, multiValueHeaders (possibly), multiValueQueryStringParameters (possibly)
* API Gateway v1 REST same as HTTP, but without version
* ALB: very similar to API Gateway v1, but requestContext contains an elb property
*/

// See https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
const restApiV1Keys = [
'body',
'headers',
const uniqueHttpApiV1Keys = [
'httpMethod',
'isBase64Encoded',
'multiValueHeaders',
'multiValueQueryStringParameters',
'path',
'pathParameters',
'queryStringParameters',
'requestContext',
'resource',
'stageVariables'
].join(',')
'multiValueHeaders',
'multiValueQueryStringParameters'
]

// See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
const httpApiV1Keys = [...restApiV1Keys.split(','), 'version'].join(',')

function isGatewayV1Event(event) {
const keys = Object.keys(event).sort().join(',')
if (keys === httpApiV1Keys && event?.version === '1.0') {
if (event?.requestContext?.elb !== undefined) {
return false
}
const hasKeys = findProperties(event, uniqueHttpApiV1Keys)
if (hasKeys && event?.version === '1.0') {
return true
}

return keys === restApiV1Keys
// Rest API doesn't have version, but we can check on the key matching:
return hasKeys
}

// See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
const httpApiV2Keys = [
'body',
'cookies',
'headers',
'isBase64Encoded',
'pathParameters',
'queryStringParameters',
'rawPath',
'rawQueryString',
'requestContext',
'routeKey',
'stageVariables',
'version'
].join(',')
const uniqueHttpApiV2Keys = ['rawPath', 'rawQueryString', 'routeKey', 'cookies']

function isGatewayV2Event(event) {
const keys = Object.keys(event).sort().join(',')
return keys === httpApiV2Keys && event?.version === '2.0'
if (event?.requestContext?.elb !== undefined) {
return false
}
return findProperties(event, uniqueHttpApiV2Keys) && event?.version === '2.0'
}
const albKeys = [
'requestContext',
'httpMethod',
'path',
'queryStringParameters',
'headers',
'body',
'isBase64Encoded',
'rawHeaders',
'multiValueQueryStringParameters',
'pathParameters'
]
.sort()
.join(',')

/**
* ALB can act as a proxy for Lambda. Properties have commonalities with API GateWay v1, though ALB-triggered events
* consistently carry an ARN at requestContext.elb. See
* https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#receive-event-from-load-balancer
* and https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html
*
* If we check for that property, we can accept variation in other properties.
*/

function isAlbEvent(event) {
const keys = Object.keys(event).sort().join(',')
return keys === albKeys && event?.requestContext?.elb !== undefined
return event?.requestContext?.elb !== undefined
}

/**
Expand Down
51 changes: 51 additions & 0 deletions test/unit/serverless/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,56 @@ const httpApiGatewayV2Event = {
}
}

const secondApiGatewayV2Event = {
version: '2.0',
routeKey: 'ANY /',
rawPath: '/dev/',
rawQueryString: '',
headers: {
'accept':
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'en-US,en;q=0.9',
'content-length': '0',
'host': 'zzz1234567890.execute-api.us-east-2.amazonaws.com',
'priority': 'u=0, i',
'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'cross-site',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'x-amzn-trace-id': 'Root=1-abcdef01-01234567890abcdef0123456',
'x-forwarded-for': '11.11.11.148',
'x-forwarded-port': '443',
'x-forwarded-proto': 'https'
},
requestContext: {
accountId: '466768951184',
apiId: 'zzz1234567890',
domainName: 'zzz1234567890.execute-api.us-east-2.amazonaws.com',
domainPrefix: 'zzz1234567890',
http: {
method: 'GET',
path: '/dev/',
protocol: 'HTTP/1.1',
sourceIp: '11.11.11.148',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
},
requestId: 'ABCDEF0123456=',
routeKey: 'ANY /',
stage: 'dev',
time: '26/Nov/2024:19:14:00 +0000',
timeEpoch: 1732648440329
},
isBase64Encoded: false
}

const albEvent = {
requestContext: {
elb: {
Expand Down Expand Up @@ -292,6 +342,7 @@ module.exports = {
restApiGatewayV1Event,
httpApiGatewayV1Event,
httpApiGatewayV2Event,
secondApiGatewayV2Event,
albEvent,
lambaV1InvocationEvent
}
24 changes: 22 additions & 2 deletions test/unit/serverless/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,44 @@
const test = require('node:test')
const assert = require('node:assert')

const { isGatewayV1Event, isGatewayV2Event } = require('../../../lib/serverless/api-gateway')
const {
isGatewayV1Event,
isGatewayV2Event,
isAlbEvent
} = require('../../../lib/serverless/api-gateway')

const {
restApiGatewayV1Event,
httpApiGatewayV1Event,
httpApiGatewayV2Event,
lambaV1InvocationEvent
secondApiGatewayV2Event,
lambaV1InvocationEvent,
albEvent
} = require('./fixtures')

test('isGatewayV1Event', () => {
assert.equal(isGatewayV1Event(restApiGatewayV1Event), true)
assert.equal(isGatewayV1Event(httpApiGatewayV1Event), true)
assert.equal(isGatewayV1Event(httpApiGatewayV2Event), false)
assert.equal(isGatewayV1Event(secondApiGatewayV2Event), false)
assert.equal(isGatewayV1Event(lambaV1InvocationEvent), false)
assert.equal(isGatewayV1Event(albEvent), false)
})

test('isGatewayV2Event', () => {
assert.equal(isGatewayV2Event(restApiGatewayV1Event), false)
assert.equal(isGatewayV2Event(httpApiGatewayV1Event), false)
assert.equal(isGatewayV2Event(httpApiGatewayV2Event), true)
assert.equal(isGatewayV2Event(secondApiGatewayV2Event), true)
assert.equal(isGatewayV2Event(lambaV1InvocationEvent), false)
assert.equal(isGatewayV2Event(albEvent), false)
})

test('isAlbEvent', () => {
assert.equal(isAlbEvent(restApiGatewayV1Event), false)
assert.equal(isAlbEvent(httpApiGatewayV1Event), false)
assert.equal(isAlbEvent(httpApiGatewayV2Event), false)
assert.equal(isAlbEvent(secondApiGatewayV2Event), false)
assert.equal(isAlbEvent(lambaV1InvocationEvent), false)
assert.equal(isAlbEvent(albEvent), true)
})

0 comments on commit ebdfb27

Please sign in to comment.