Skip to content

Commit

Permalink
Merge pull request #1092 from NicolasSeiler/apigatewayv2-payload-2.0
Browse files Browse the repository at this point in the history
Add support for Apigatewayv2 payload 2.0
  • Loading branch information
dherault committed Sep 23, 2020
2 parents 2b4dd1e + 60a711b commit 5060bda
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 12 deletions.
12 changes: 11 additions & 1 deletion src/ServerlessOffline.js
Expand Up @@ -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) {
Expand Down
54 changes: 43 additions & 11 deletions src/events/http/HttpServer.js
Expand Up @@ -24,6 +24,7 @@ import {
splitHandlerPathAndName,
generateHapiPath,
} from '../../utils/index.js'
import LambdaProxyIntegrationEventV2 from './lambda-events/LambdaProxyIntegrationEventV2.js'

const { parse, stringify } = JSON

Expand Down Expand Up @@ -538,7 +539,13 @@ export default class HttpServer {
const stageVariables = this.#serverless.service.custom
? this.#serverless.service.custom.stageVariables
: null
const lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent(

const LambdaProxyEvent =
endpoint.isHttpApi && endpoint.payload === '2.0'
? LambdaProxyIntegrationEventV2
: LambdaProxyIntegrationEvent

const lambdaProxyIntegrationEvent = new LambdaProxyEvent(
request,
this.#serverless.service.provider.stage,
requestPath,
Expand Down Expand Up @@ -790,6 +797,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 {
Expand All @@ -816,18 +840,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,
Expand All @@ -837,6 +861,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,
Expand Down
179 changes: 179 additions & 0 deletions 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,
}
}
}

0 comments on commit 5060bda

Please sign in to comment.