|
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