|
1 | 1 | import awsLambdaFastify from "@fastify/aws-lambda"; |
| 2 | +import { pipeline } from "node:stream/promises"; |
2 | 3 | import init, { instanceId } from "./index.js"; |
3 | | -import { type APIGatewayEvent, type Context } from "aws-lambda"; |
4 | 4 | import { InternalServerError, ValidationError } from "common/errors/index.js"; |
| 5 | +import { Readable } from "node:stream"; |
5 | 6 |
|
| 7 | +// Initialize the proxy with the payloadAsStream option |
6 | 8 | const app = await init(); |
7 | | -const realHandler = awsLambdaFastify(app, { |
| 9 | +const proxy = awsLambdaFastify(app, { |
| 10 | + payloadAsStream: true, |
8 | 11 | decorateRequest: false, |
9 | | - serializeLambdaArguments: true, |
10 | 12 | callbackWaitsForEmptyEventLoop: false, |
11 | | - binaryMimeTypes: ["application/octet-stream", "application/vnd.apple.pkpass"], |
| 13 | + serializeLambdaArguments: true, |
| 14 | + binaryMimeTypes: ["application/octet-stream", "application/vnd.apple.pkpass"], // from original code |
12 | 15 | }); |
13 | 16 |
|
14 | | -type WarmerEvent = { action: "warmer" }; |
15 | | - |
16 | | -/** |
17 | | - * Validates the origin verification header against the current and previous keys. |
18 | | - * @returns {boolean} `true` if the request is valid, otherwise `false`. |
19 | | - */ |
20 | 17 | const validateOriginHeader = ( |
21 | | - originHeader: string | undefined, |
| 18 | + originHeader: string, |
22 | 19 | currentKey: string, |
23 | 20 | previousKey: string | undefined, |
24 | 21 | previousKeyExpiresAt: string | undefined, |
25 | | -): boolean => { |
26 | | - // 1. A header must exist to be valid. |
| 22 | +) => { |
27 | 23 | if (!originHeader) { |
28 | 24 | return false; |
29 | 25 | } |
30 | | - |
31 | | - // 2. Check against the current key first for an early return on the happy path. |
32 | 26 | if (originHeader === currentKey) { |
33 | 27 | return true; |
34 | 28 | } |
35 | | - |
36 | | - // 3. If it's not the current key, check the previous key during the rotation window. |
37 | 29 | if (previousKey && previousKeyExpiresAt) { |
38 | 30 | const isExpired = new Date() >= new Date(previousKeyExpiresAt); |
39 | 31 | if (originHeader === previousKey && !isExpired) { |
40 | 32 | return true; |
41 | 33 | } |
42 | 34 | } |
43 | | - |
44 | | - // 4. If all checks fail, the header is invalid. |
45 | 35 | return false; |
46 | 36 | }; |
47 | 37 |
|
48 | | -const handler = async ( |
49 | | - event: APIGatewayEvent | WarmerEvent, |
50 | | - context: Context, |
51 | | -) => { |
52 | | - if ("action" in event && event.action === "warmer") { |
53 | | - return { instanceId }; |
54 | | - } |
55 | | - event = event as APIGatewayEvent; |
| 38 | +// This handler now correctly uses the native streaming support from the packages. |
| 39 | +export const handler = awslambda.streamifyResponse( |
| 40 | + async (event: any, responseStream: any, context: any) => { |
| 41 | + context.callbackWaitsForEmptyEventLoop = false; |
| 42 | + if ("action" in event && event.action === "warmer") { |
| 43 | + const requestStream = Readable.from( |
| 44 | + Buffer.from(JSON.stringify({ instanceId })), |
| 45 | + ); |
| 46 | + await pipeline(requestStream, responseStream); |
| 47 | + return; |
| 48 | + } |
56 | 49 |
|
57 | | - const currentKey = process.env.ORIGIN_VERIFY_KEY; |
58 | | - const previousKey = process.env.PREVIOUS_ORIGIN_VERIFY_KEY; |
59 | | - const previousKeyExpiresAt = |
60 | | - process.env.PREVIOUS_ORIGIN_VERIFY_KEY_EXPIRES_AT; |
| 50 | + // 2. Perform origin header validation before calling the proxy |
| 51 | + const currentKey = process.env.ORIGIN_VERIFY_KEY; |
| 52 | + if (currentKey) { |
| 53 | + const previousKey = process.env.PREVIOUS_ORIGIN_VERIFY_KEY; |
| 54 | + const previousKeyExpiresAt = |
| 55 | + process.env.PREVIOUS_ORIGIN_VERIFY_KEY_EXPIRES_AT; |
61 | 56 |
|
62 | | - // Log an error if the previous key has expired but is still configured. |
63 | | - if (previousKey && previousKeyExpiresAt) { |
64 | | - if (new Date() >= new Date(previousKeyExpiresAt)) { |
65 | | - console.error( |
66 | | - "Expired previous origin verify key is still present in the environment. Expired at:", |
| 57 | + const isValid = validateOriginHeader( |
| 58 | + event.headers?.["x-origin-verify"], |
| 59 | + currentKey, |
| 60 | + previousKey, |
67 | 61 | previousKeyExpiresAt, |
68 | 62 | ); |
69 | | - } |
70 | | - } |
71 | 63 |
|
72 | | - // Proceed with verification only if a current key is set. |
73 | | - if (currentKey) { |
74 | | - const isValid = validateOriginHeader( |
75 | | - event.headers?.["x-origin-verify"], |
76 | | - currentKey, |
77 | | - previousKey, |
78 | | - previousKeyExpiresAt, |
79 | | - ); |
| 64 | + if (!isValid) { |
| 65 | + const error = new ValidationError({ message: "Request is not valid." }); |
| 66 | + const body = JSON.stringify(error.toJson()); |
80 | 67 |
|
81 | | - if (!isValid) { |
82 | | - const newError = new ValidationError({ |
83 | | - message: "Request is not valid.", |
84 | | - }); |
85 | | - const json = JSON.stringify(newError.toJson()); |
86 | | - return { |
87 | | - statusCode: newError.httpStatusCode, |
88 | | - body: json, |
89 | | - headers: { |
90 | | - "Content-Type": "application/json", |
91 | | - }, |
92 | | - isBase64Encoded: false, |
93 | | - }; |
| 68 | + // On validation failure, manually create the response |
| 69 | + const meta = { |
| 70 | + statusCode: error.httpStatusCode, |
| 71 | + headers: { "Content-Type": "application/json" }, |
| 72 | + }; |
| 73 | + responseStream = awslambda.HttpResponseStream.from( |
| 74 | + responseStream, |
| 75 | + meta, |
| 76 | + ); |
| 77 | + const requestStream = Readable.from(Buffer.from(body)); |
| 78 | + await pipeline(requestStream, responseStream); |
| 79 | + return; |
| 80 | + } |
| 81 | + delete event.headers["x-origin-verify"]; |
94 | 82 | } |
95 | 83 |
|
96 | | - delete event.headers["x-origin-verify"]; |
97 | | - } |
98 | | - |
99 | | - // If verification is disabled or passed, proceed with the real handler logic. |
100 | | - return await realHandler(event, context).catch((e) => { |
101 | | - console.error(e); |
102 | | - const newError = new InternalServerError({ |
103 | | - message: "Failed to initialize application.", |
104 | | - }); |
105 | | - const json = JSON.stringify(newError.toJson()); |
106 | | - return { |
107 | | - statusCode: newError.httpStatusCode, |
108 | | - body: json, |
109 | | - headers: { |
110 | | - "Content-Type": "application/json", |
111 | | - }, |
112 | | - isBase64Encoded: false, |
113 | | - }; |
114 | | - }); |
115 | | -}; |
116 | | - |
117 | | -await app.ready(); |
118 | | -export { handler }; |
| 84 | + const { stream, meta } = await proxy(event, context); |
| 85 | + // Fix issue with Lambda where streaming repsonses always require a body to be present |
| 86 | + const body = |
| 87 | + stream.readableLength > 0 ? stream : Readable.from(Buffer.from(" ")); |
| 88 | + responseStream = awslambda.HttpResponseStream.from( |
| 89 | + responseStream, |
| 90 | + meta as any, |
| 91 | + ); |
| 92 | + await pipeline(body, responseStream); |
| 93 | + }, |
| 94 | +); |
0 commit comments