UNPKG

serverless-offline

Version:

Emulate AWS λ and API Gateway locally when developing your Serverless project

199 lines (171 loc) 5.65 kB
import { Buffer } from "node:buffer" import { env } from "node:process" import { decodeJwt } from "jose" import { log } from "../../../utils/log.js" import { detectEncoding, formatToClfTime, lowerCaseKeys, nullIfEmpty, parseHeaders, parseQueryStringParametersForPayloadV2, } from "../../../utils/index.js" const { isArray } = Array const { parse } = JSON const { assign, entries } = 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 { #additionalRequestContext = null #routeKey = null #request = null #stage = null constructor(request, stage, routeKey, additionalRequestContext) { this.#additionalRequestContext = additionalRequestContext || {} this.#routeKey = routeKey this.#request = request this.#stage = stage } create() { const authContext = (this.#request.auth && this.#request.auth.credentials && this.#request.auth.credentials.context) || {} // AWS adds the lambda key to the auth context object const lambdaAuthContext = { lambda: authContext } let authAuthorizer if (env.AUTHORIZER) { try { authAuthorizer = parse(env.AUTHORIZER) } catch { log.error( "Could not parse process.env.AUTHORIZER, make sure it is correct JSON", ) } } let body = this.#request.payload let isBase64Encoded = false const { rawHeaders } = this.#request.raw.req // NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject()) const headers = lowerCaseKeys(parseHeaders(rawHeaders || [])) || {} if (headers["sls-offline-authorizer-override"]) { try { authAuthorizer = parse(headers["sls-offline-authorizer-override"]) } catch { log.error( "Could not parse header sls-offline-authorizer-override, make sure it is correct JSON", ) } } if (body) { if ( this.#request.raw.req.payload && detectEncoding(this.#request) === "binary" ) { body = Buffer.from(this.#request.raw.req.payload).toString("base64") headers["content-length"] = String(Buffer.byteLength(body, "base64")) isBase64Encoded = true } if (typeof body !== "string") { // this.#request.payload is NOT the same as the rawPayload body = this.#request.rawPayload } if ( !headers["content-length"] && (typeof body === "string" || body instanceof Buffer || body instanceof ArrayBuffer) ) { headers["content-length"] = String(Buffer.byteLength(body)) } // Set a default Content-Type if not provided. if (!headers["content-type"]) { headers["content-type"] = "application/json" } } else if (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 = decodeJwt(token) if (claims.scp || claims.scope) { scopes = claims.scp || 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 { // Do nothing } } const { headers: _headers, info: { received, remoteAddress }, method, } = this.#request const httpMethod = method.toUpperCase() const requestTime = formatToClfTime(received) const requestTimeEpoch = received const cookies = this.#request.state ? entries(this.#request.state).flatMap(([key, value]) => { if (isArray(value)) { return value.map((v) => `${key}=${v}`) } return `${key}=${value}` }) : undefined return { body, cookies, headers, isBase64Encoded, pathParameters: nullIfEmpty(pathParams), queryStringParameters: this.#request.url.search ? parseQueryStringParametersForPayloadV2(this.#request.url.searchParams) : null, rawPath: this.#request.url.pathname, rawQueryString: this.#request.url.searchParams.toString(), requestContext: { accountId: "offlineContext_accountId", apiId: "offlineContext_apiId", authorizer: authAuthorizer || assign(lambdaAuthContext, { jwt: { claims, scopes, }, }), domainName: "offlineContext_domainName", domainPrefix: "offlineContext_domainPrefix", http: { method: httpMethod, path: this.#request.url.pathname, protocol: "HTTP/1.1", sourceIp: remoteAddress, userAgent: _headers["user-agent"] || "", }, operationName: this.#additionalRequestContext.operationName, requestId: "offlineContext_resourceId", routeKey: this.#routeKey, stage: this.#stage, time: requestTime, timeEpoch: requestTimeEpoch, }, routeKey: this.#routeKey, stageVariables: null, version: "2.0", } } }