Skip to content

Commit

Permalink
feat: convert multipart/form-data to base64 encoded payloads (#1776)
Browse files Browse the repository at this point in the history
* base64 encode binary

* add a test

* use rawPayload and set content length

* store raw payload in raw.req

* fix payload version 1.0
  • Loading branch information
cnuss committed Apr 27, 2024
1 parent d8cb9ba commit 2d9dbc2
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/events/http/HttpServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ export default class HttpServer {
// payload processing
const encoding = detectEncoding(request)

request.raw.req.payload = request.payload
request.payload = request.payload && request.payload.toString(encoding)
request.rawPayload = request.payload

Expand Down
13 changes: 12 additions & 1 deletion src/events/http/lambda-events/LambdaProxyIntegrationEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { env } from "node:process"
import { log } from "@serverless/utils/log.js"
import { decodeJwt } from "jose"
import {
detectEncoding,
formatToClfTime,
nullIfEmpty,
parseHeaders,
Expand Down Expand Up @@ -63,6 +64,7 @@ export default class LambdaProxyIntegrationEvent {
}

let body = this.#request.payload
let isBase64Encoded = false

const { rawHeaders, url } = this.#request.raw.req

Expand All @@ -80,6 +82,15 @@ export default class LambdaProxyIntegrationEvent {
}

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
Expand Down Expand Up @@ -155,7 +166,7 @@ export default class LambdaProxyIntegrationEvent {
body,
headers,
httpMethod,
isBase64Encoded: false, // TODO hook up
isBase64Encoded,
multiValueHeaders: parseMultiValueHeaders(
// NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject())
rawHeaders || [],
Expand Down
13 changes: 12 additions & 1 deletion src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { env } from "node:process"
import { log } from "@serverless/utils/log.js"
import { decodeJwt } from "jose"
import {
detectEncoding,
formatToClfTime,
lowerCaseKeys,
nullIfEmpty,
Expand Down Expand Up @@ -55,6 +56,7 @@ export default class LambdaProxyIntegrationEventV2 {
}

let body = this.#request.payload
let isBase64Encoded = false

const { rawHeaders } = this.#request.raw.req

Expand All @@ -72,6 +74,15 @@ export default class LambdaProxyIntegrationEventV2 {
}

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
Expand Down Expand Up @@ -145,7 +156,7 @@ export default class LambdaProxyIntegrationEventV2 {
body,
cookies,
headers,
isBase64Encoded: false,
isBase64Encoded,
pathParameters: nullIfEmpty(pathParams),
queryStringParameters: this.#request.url.search
? parseQueryStringParametersForPayloadV2(this.#request.url.searchParams)
Expand Down
73 changes: 67 additions & 6 deletions tests/integration/httpApi-headers/httpApi-headers.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from "node:assert"
import { Buffer } from "node:buffer"
import { join } from "desm"
import { setup, teardown } from "../../_testHelpers/index.js"
import { BASE_URL } from "../../config.js"
Expand All @@ -13,15 +14,60 @@ describe("HttpApi Headers Tests", function desc() {
afterEach(() => teardown())

//
;["GET", "POST"].forEach((method) => {
it(`${method} headers`, async () => {
const url = new URL("/echo-headers", BASE_URL)
;[
{
desiredContentLengthHeader: "Content-Length",
desiredOriginHeader: "Origin",
desiredWebhookSignatureHeder: "X-Webhook-Signature",
payloadVersion: "1.0",
},
{
desiredContentLengthHeader: "content-length",
desiredOriginHeader: "origin",
desiredWebhookSignatureHeder: "x-webhook-signature",
payloadVersion: "2.0",
},
].forEach((t) => {
;["GET", "POST"].forEach((method) => {
it(`${method} headers (payload ${t.payloadVersion})`, async () => {
const url = new URL(`/echo-headers-${t.payloadVersion}`, BASE_URL)
const options = {
headers: {
Origin: "http://www.example.com",
"X-Webhook-Signature": "ABCDEF",
},
method,
}

const response = await fetch(url, options)

assert.equal(response.status, 200)

const body = await response.json()

assert.equal(
body.headersReceived[t.desiredOriginHeader],
"http://www.example.com",
)
assert.equal(
body.headersReceived[t.desiredWebhookSignatureHeder],
"ABCDEF",
)
assert.equal(body.isBase64EncodedReceived, false)
})
})

it(`multipart/form-data headers are base64 encoded (payload ${t.payloadVersion})`, async () => {
const url = new URL(`/echo-headers-${t.payloadVersion}`, BASE_URL)
const options = {
body: `------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="file"; filename="file.txt"\r\nContent-Type: text/plain\r\n\r\n\u0001content\u0003\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n`,
headers: {
"Content-Type":
"multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
Origin: "http://www.example.com",
"X-Webhook-Signature": "ABCDEF",
},
method,
method: "POST",
}

const response = await fetch(url, options)
Expand All @@ -30,8 +76,23 @@ describe("HttpApi Headers Tests", function desc() {

const body = await response.json()

assert.equal(body.headersReceived.origin, "http://www.example.com")
assert.equal(body.headersReceived["x-webhook-signature"], "ABCDEF")
assert.equal(
body.headersReceived[t.desiredOriginHeader],
"http://www.example.com",
)
assert.equal(
body.headersReceived[t.desiredWebhookSignatureHeder],
"ABCDEF",
)
assert.equal(
Number.parseInt(body.headersReceived[t.desiredContentLengthHeader], 10),
options.body.length,
)
assert.equal(body.isBase64EncodedReceived, true)
assert.equal(
body.bodyReceived,
Buffer.from(options.body).toString("base64"),
)
})
})
})
19 changes: 15 additions & 4 deletions tests/integration/httpApi-headers/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ plugins:
provider:
architecture: arm64
deploymentMethod: direct
httpApi:
payload: "2.0"
memorySize: 1024
name: aws
region: us-east-1
Expand All @@ -19,12 +17,25 @@ provider:
versionFunctions: false

functions:
echoHeadersV1:
httpApi:
payload: "1.0"
events:
- httpApi:
method: get
path: /echo-headers-1.0
- httpApi:
method: post
path: /echo-headers-1.0
handler: src/handler.echoHeaders
echoHeaders:
httpApi:
payload: "2.0"
events:
- httpApi:
method: get
path: /echo-headers
path: /echo-headers-2.0
- httpApi:
method: post
path: /echo-headers
path: /echo-headers-2.0
handler: src/handler.echoHeaders
2 changes: 2 additions & 0 deletions tests/integration/httpApi-headers/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ const { stringify } = JSON
export async function echoHeaders(event) {
return {
body: stringify({
bodyReceived: event.body,
headersReceived: event.headers,
isBase64EncodedReceived: event.isBase64Encoded,
}),
statusCode: 200,
}
Expand Down

0 comments on commit 2d9dbc2

Please sign in to comment.