From 34976a7e829d390f227a5d2f6574f62008c538a4 Mon Sep 17 00:00:00 2001 From: "nicolas8991@gmail.com" Date: Sat, 5 Sep 2020 19:30:05 +0200 Subject: [PATCH 1/5] Add ApiGatewayV2 payload 2.0 request --- src/ServerlessOffline.js | 12 +- src/events/http/HttpServer.js | 24 ++- .../LambdaProxyIntegrationEventV2.js | 179 ++++++++++++++++++ 3 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js diff --git a/src/ServerlessOffline.js b/src/ServerlessOffline.js index 6aad6cf14..391eb7b61 100644 --- a/src/ServerlessOffline.js +++ b/src/ServerlessOffline.js @@ -296,7 +296,17 @@ export default class ServerlessOffline { // will not have the isHttpApi flag set. This will need to be addressed // when adding support for HttpApi 2.0 payload types. if (httpApi && typeof httpApi === 'object') { - httpEvent.http = { ...httpApi, isHttpApi: true } + httpEvent.http = { + ...httpApi, + isHttpApi: true, + } + + if (!httpEvent.http.payload) { + if (service.provider.httpApi) { + httpEvent.http.payload = + service.provider.httpApi.payload || '1.0' + } + } } if (http && http.private) { diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 255be0e2c..77efa8552 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -24,6 +24,7 @@ import { splitHandlerPathAndName, generateHapiPath, } from '../../utils/index.js' +import LambdaProxyIntegrationEventV2 from './lambda-events/LambdaProxyIntegrationEventV2.js' const { parse, stringify } = JSON @@ -538,12 +539,23 @@ export default class HttpServer { const stageVariables = this.#serverless.service.custom ? this.#serverless.service.custom.stageVariables : null - const lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent( - request, - this.#serverless.service.provider.stage, - requestPath, - stageVariables, - ) + let lambdaProxyIntegrationEvent + + if (endpoint.isHttpApi && endpoint.payload === '2.0') { + lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEventV2( + request, + this.#serverless.service.provider.stage, + requestPath, + stageVariables, + ) + } else { + lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent( + request, + this.#serverless.service.provider.stage, + requestPath, + stageVariables, + ) + } event = lambdaProxyIntegrationEvent.create() } diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js new file mode 100644 index 000000000..5690badc9 --- /dev/null +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js @@ -0,0 +1,179 @@ +import { Buffer } from 'buffer' +import { decode } from 'jsonwebtoken' +import { + formatToClfTime, + nullIfEmpty, + parseHeaders, + parseQueryStringParameters, +} from '../../../utils/index.js' +import { BASE_URL_PLACEHOLDER } from '../../../config/index.js' + +const { byteLength } = Buffer +const { parse } = JSON +const { assign } = Object + +// https://www.serverless.com/framework/docs/providers/aws/events/http-api/ +// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html +export default class LambdaProxyIntegrationEventV2 { + #path = null + #request = null + #stage = null + #stageVariables = null + + constructor(request, stage, path, stageVariables) { + this.#path = path + this.#request = request + this.#stage = stage + this.#stageVariables = stageVariables + } + + create() { + const authPrincipalId = + this.#request.auth && + this.#request.auth.credentials && + this.#request.auth.credentials.principalId + + const authContext = + (this.#request.auth && + this.#request.auth.credentials && + this.#request.auth.credentials.context) || + {} + + let authAuthorizer + + if (process.env.AUTHORIZER) { + try { + authAuthorizer = parse(process.env.AUTHORIZER) + } catch (error) { + console.error( + 'Serverless-offline: Could not parse process.env.AUTHORIZER, make sure it is correct JSON.', + ) + } + } + + let body = this.#request.payload + + const { rawHeaders, url } = this.#request.raw.req + + // NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject()) + const headers = parseHeaders(rawHeaders || []) || {} + + if (body) { + if (typeof body !== 'string') { + // this.#request.payload is NOT the same as the rawPayload + body = this.#request.rawPayload + } + + if ( + !headers['Content-Length'] && + !headers['content-length'] && + !headers['Content-length'] && + (typeof body === 'string' || + body instanceof Buffer || + body instanceof ArrayBuffer) + ) { + headers['Content-Length'] = String(byteLength(body)) + } + + // Set a default Content-Type if not provided. + if ( + !headers['Content-Type'] && + !headers['content-type'] && + !headers['Content-type'] + ) { + headers['Content-Type'] = 'application/json' + } + } else if (typeof body === 'undefined' || body === '') { + body = null + } + + // clone own props + const pathParams = { ...this.#request.params } + + let token = headers.Authorization || headers.authorization + + if (token && token.split(' ')[0] === 'Bearer') { + ;[, token] = token.split(' ') + } + + let claims + let scopes + + if (token) { + try { + claims = decode(token) || undefined + if (claims && claims.scope) { + scopes = claims.scope.split(' ') + // In AWS HTTP Api the scope property is removed from the decoded JWT + // I'm leaving this property because I'm not sure how all of the authorizers + // for AWS REST Api handle JWT. + // claims = { ...claims } + // delete claims.scope + } + } catch (err) { + // Do nothing + } + } + + const { + headers: _headers, + info: { received, remoteAddress }, + method, + route, + } = this.#request + + const httpMethod = method.toUpperCase() + const requestTime = formatToClfTime(received) + const requestTimeEpoch = received + + const cookies = Object.entries(this.#request.state).map( + ([key, value]) => `${key}=${value}`, + ) + + return { + version: '2.0', + routeKey: this.#path, + rawPath: route.path, + rawQueryString: new URL( + url, + BASE_URL_PLACEHOLDER, + ).searchParams.toString(), + cookies, + headers, + queryStringParameters: parseQueryStringParameters(url), + requestContext: { + accountId: 'offlineContext_accountId', + apiId: 'offlineContext_apiId', + authorizer: + authAuthorizer || + assign(authContext, { + claims, + scopes, + // 'principalId' should have higher priority + principalId: + authPrincipalId || + process.env.PRINCIPAL_ID || + 'offlineContext_authorizer_principalId', // See #24 + }), + domainName: 'offlineContext_domainName', + domainPrefix: 'offlineContext_domainPrefix', + http: { + method: httpMethod, + path: this.#path, + protocol: 'HTTP/1.1', + sourceIp: remoteAddress, + userAgent: _headers['user-agent'] || '', + }, + requestId: 'offlineContext_resourceId', + routeKey: this.#path, + stage: this.#stage, + time: requestTime, + timeEpoch: requestTimeEpoch, + }, + body, + pathParameters: nullIfEmpty(pathParams), + isBase64Encoded: false, + stageVariables: this.#stageVariables, + } + } +} From d86766e5f1f025d79d4b2e49becdb11fa6f6bf2c Mon Sep 17 00:00:00 2001 From: "nicolas8991@gmail.com" Date: Sat, 5 Sep 2020 21:06:45 +0200 Subject: [PATCH 2/5] Handle apigatewayv2 response payload format 2.0 --- src/events/http/HttpServer.js | 45 +++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 77efa8552..f3e50f7b0 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -802,6 +802,23 @@ export default class HttpServer { } else if (integration === 'AWS_PROXY') { /* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */ + if ( + endpoint.isHttpApi && + endpoint.payload === '2.0' && + (typeof result === 'string' || !result.statusCode) + ) { + const body = + typeof result === 'string' ? result : JSON.stringify(result) + result = { + isBase64Encoded: false, + statusCode: 200, + body, + headers: { + 'Content-Type': 'application/json', + }, + } + } + if (result && !result.errorType) { statusCode = result.statusCode || 200 } else { @@ -828,18 +845,18 @@ export default class HttpServer { debugLog('headers', headers) + const parseCookies = (headerValue) => { + const cookieName = headerValue.slice(0, headerValue.indexOf('=')) + const cookieValue = headerValue.slice(headerValue.indexOf('=') + 1) + h.state(cookieName, cookieValue, { + encoding: 'none', + strictHeader: false, + }) + } + Object.keys(headers).forEach((header) => { if (header.toLowerCase() === 'set-cookie') { - headers[header].forEach((headerValue) => { - const cookieName = headerValue.slice(0, headerValue.indexOf('=')) - const cookieValue = headerValue.slice( - headerValue.indexOf('=') + 1, - ) - h.state(cookieName, cookieValue, { - encoding: 'none', - strictHeader: false, - }) - }) + headers[header].forEach(parseCookies) } else { headers[header].forEach((headerValue) => { // it looks like Hapi doesn't support multiple headers with the same name, @@ -849,6 +866,14 @@ export default class HttpServer { } }) + if ( + endpoint.isHttpApi && + endpoint.payload === '2.0' && + result.cookies + ) { + result.cookies.forEach(parseCookies) + } + response.header('Content-Type', 'application/json', { duplicate: false, override: false, From 129d6c7b70f23958aedb3e51e3a4d6a073483b5e Mon Sep 17 00:00:00 2001 From: "nicolas8991@gmail.com" Date: Sun, 6 Sep 2020 13:35:26 +0200 Subject: [PATCH 3/5] Trigger build --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 30a2cc18c..4774e9859 100644 --- a/package.json +++ b/package.json @@ -212,3 +212,4 @@ "serverless": ">=1.60.0" } } + From 0646865264ce31040b87cf7bf4bad94e7ec8b010 Mon Sep 17 00:00:00 2001 From: "nicolas8991@gmail.com" Date: Sun, 6 Sep 2020 13:36:00 +0200 Subject: [PATCH 4/5] Trigger build --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 4774e9859..30a2cc18c 100644 --- a/package.json +++ b/package.json @@ -212,4 +212,3 @@ "serverless": ">=1.60.0" } } - From 60a711b77cdaaa3ac171a9c1e54ad753ac76750b Mon Sep 17 00:00:00 2001 From: "nicolas8991@gmail.com" Date: Sun, 6 Sep 2020 22:51:54 +0200 Subject: [PATCH 5/5] Add back const --- src/events/http/HttpServer.js | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index f3e50f7b0..0c77db282 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -539,23 +539,18 @@ export default class HttpServer { const stageVariables = this.#serverless.service.custom ? this.#serverless.service.custom.stageVariables : null - let lambdaProxyIntegrationEvent - - if (endpoint.isHttpApi && endpoint.payload === '2.0') { - lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEventV2( - request, - this.#serverless.service.provider.stage, - requestPath, - stageVariables, - ) - } else { - lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent( - request, - this.#serverless.service.provider.stage, - requestPath, - stageVariables, - ) - } + + const LambdaProxyEvent = + endpoint.isHttpApi && endpoint.payload === '2.0' + ? LambdaProxyIntegrationEventV2 + : LambdaProxyIntegrationEvent + + const lambdaProxyIntegrationEvent = new LambdaProxyEvent( + request, + this.#serverless.service.provider.stage, + requestPath, + stageVariables, + ) event = lambdaProxyIntegrationEvent.create() }