-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerateLaunch.js
More file actions
322 lines (281 loc) · 11.1 KB
/
generateLaunch.js
File metadata and controls
322 lines (281 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
"use strict";
// -----------------------------------------------------------------------------
// Advanced Configurables
// -----------------------------------------------------------------------------
// NODE_OPTIONS is set to use the OpenSSL legacy provider for serverless webpack.
// Change the value here if you require a different OpenSSL configuration.
process.env.NODE_OPTIONS = "--openssl-legacy-provider";
// S3 configuration - change these values if needed.
const S3_REGION = "us-east-1";
const BUCKET_NAME = "cloudfront-integration-bundles";
// -----------------------------------------------------------------------------
// Module Imports
// -----------------------------------------------------------------------------
const crypto = require("crypto");
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const s3 = new S3Client({ region: S3_REGION });
const express = require("express");
const axios = require("axios");
const { exec } = require("child_process");
const cors = require("cors");
const fs = require("fs");
const path = require("path");
// Initialize Express app
const app = express();
const port = process.env.PORT || 3000;
// Enable CORS for all routes
app.use(cors());
// Middleware to parse JSON bodies
app.use(express.json());
// -----------------------------------------------------------------------------
// Utility Functions
// -----------------------------------------------------------------------------
/**
* Executes a shell command and returns a promise that resolves with the output.
*
* @param {string} cmd - The command to execute.
* @returns {Promise<{ stdout: string, stderr: string }>} - Resolves with the command output.
*/
function execPromise(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout, stderr) => {
if (err) return reject({ err, stdout, stderr });
resolve({ stdout, stderr });
});
});
}
/**
* Uploads a single file to S3 under the specified directory (prefix).
*
* @param {string} filePath - The local file path of the file to upload.
* @param {string} directory - The S3 directory (prefix) in which to store the file.
* @returns {Promise<void>}
*/
async function uploadFileToS3(filePath, directory) {
const fileName = path.basename(filePath);
// Ensure the directory ends with a slash to mimic folder structure in S3.
if (!directory.endsWith("/")) {
directory += "/";
}
const key = `${directory}${fileName}`;
// Get file statistics to determine its size.
const stat = fs.statSync(filePath);
const fileStream = fs.createReadStream(filePath);
const command = new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
Body: fileStream,
ContentType: "application/zip",
ContentLength: stat.size,
});
try {
await s3.send(command);
console.log(`Successfully uploaded ${fileName} to ${BUCKET_NAME}/${key}`);
} catch (err) {
console.error(`Error uploading ${fileName}:`, err);
throw err;
}
}
/**
* Verifies the public key by checking its format and hashing the second half with a salt.
*
* @param {string} key - Public Key to verify.
* @returns {boolean|object} - Returns false if the key is invalid, otherwise returns an object with key details.
* @throws {Error} - Throws an error if the key is not valid.
*/
function verifyPubKey(key) {
let salt = process.env.SALT;
let cognitoID = null;
if (!key) {
console.log("No key provided.");
return false;
}
// Deal with invalid keys.
if (key.length !== 64) {
console.log("Key is not 64 characters!");
console.log(key);
return false;
}
// Get first half of the key
const keyFirstPortion = key.slice(0, 32);
const keySecondPortion = key.slice(32);
// Use crypto for MD5 hashing
const keySecondPortionSalted = crypto
.createHash("md5")
.update(`${keySecondPortion}${salt}`)
.digest("hex");
// Salted second half of the original key should match the first half
if (keySecondPortionSalted !== keyFirstPortion) {
console.log("Key mismatch!");
return false;
} else {
// These values have been injected during the key creation process to assist with database sharding.
return {
cognitoID: cognitoID,
key: key,
shard: keySecondPortion[7],
territory: keySecondPortion[15],
};
}
}
/**
* Uploads multiple files to S3 by iterating over each file.
*
* @param {string[]} files - An array of local file paths to upload.
* @param {string} directory - The S3 directory (prefix) where the files should be uploaded.
* @returns {Promise<void>}
*/
async function uploadFiles(files, directory) {
for (const filePath of files) {
await uploadFileToS3(filePath, directory);
}
}
/**
* Retrieves the latest version of the function code from the GitHub repository.
*
* @returns {Promise<string>} - Resolves with the function code as a string.
*/
async function getFunctionCode() {
const rawCodeURL =
"https://raw.githubusercontent.com/Crowdhandler/crowdhandler-cloudfront-integration/refs/heads/master/handlerViewerRequest.js";
const response = await axios.get(rawCodeURL);
return response.data;
}
/**
* Packages the garnished code using the serverless framework.
* This function temporarily renames files to ensure correct packaging.
*
* @returns {Promise<string>} - Resolves with the stdout output of the packaging command.
*/
async function serverlessPackage() {
const orig = "handlerViewerRequest.js";
const base = "handlerViewerRequest.js.base";
const garnished = "handlerViewerRequest.js.garnished";
// Rename originals → base, garnished → orig
fs.renameSync(orig, base);
fs.renameSync(garnished, orig);
try {
const { stdout, stderr } = await execPromise(
"serverless package --package garnished_dist"
);
console.log("serverless stdout:", stdout);
console.log("serverless stderr:", stderr);
return stdout;
} catch (e) {
console.error("Error during serverless package:", e.stderr || e.err);
throw e;
} finally {
// Always restore base → orig
if (fs.existsSync(base)) {
fs.renameSync(base, orig);
}
}
}
// -----------------------------------------------------------------------------
// Express Route Handler
// -----------------------------------------------------------------------------
/**
* GET /generateQuickLaunchURL
*
* This endpoint:
* - Pulls down the latest function code from GitHub.
* - Replaces placeholders (CROWDHANDLER_PUBLIC_KEY, CROWDHANDLER_API_DOMAIN) using query parameters.
* - Writes the garnished code to a file.
* - Packages the code using the serverless framework.
* - Uploads the generated zip files to S3 under a key-scoped directory.
* - Returns a Quick Launch URL for deploying the stack via CloudFormation.
*
* Query Parameters:
* - publicKey (required): The public key used to scope the S3 directory.
* - apiDomain (optional): The API domain to replace in the code (default: "api.crowdhandler.com").
*
* @example GET /generateQuickLaunchURL?publicKey=YOUR_PUBLIC_KEY&apiDomain=your.api.domain
*/
app.get("/generateQuickLaunchURL", async (req, res) => {
try {
const publicKey = req.query.publicKey;
const apiDomain = req.query.apiDomain || "api.crowdhandler.com";
if (!publicKey) {
return res.status(400).send("Public Key is required");
}
// Verify the public key.
try {
let keyStatus = verifyPubKey(publicKey);
if (!keyStatus) {
throw new Error("Invalid public key");
}
} catch (error) {
console.error("Error verifying public key:", error);
return res.status(400).send("Invalid Public Key");
}
// Pull down the latest version of the function code from GitHub.
let code = await getFunctionCode();
// Replace placeholders with actual values.
const garnishedCode = code
.replace("CROWDHANDLER_PUBLIC_KEY", publicKey)
.replace("CROWDHANDLER_API_DOMAIN", apiDomain);
// Write the modified code to a file.
fs.writeFileSync("handlerViewerRequest.js.garnished", garnishedCode);
// Package the garnished code using serverless.
await serverlessPackage();
// Upload the zip files to S3 under the path: dist/<publicKey>/*.zip
await uploadFiles(
[
"garnished_dist/originOverride.zip",
"garnished_dist/viewerRequest.zip",
"garnished_dist/viewerResponse.zip",
"garnished_dist/originResponse.zip",
],
`dist/${publicKey}`
);
// Construct a Quick Launch URL for CloudFormation.
// The CloudFormation template is hardcoded to a fixed location in our S3 bucket.
const templateURL =
"https://cloudfront-integration-bundles.s3.us-east-1.amazonaws.com/cloudformation.yaml";
const quickLaunchURL = `https://console.aws.amazon.com/cloudformation/home?region=${S3_REGION}#/stacks/create/review?templateURL=${encodeURIComponent(
templateURL
)}&stackName=crowdhandler¶m_PublicKey=${publicKey}`;
// Return the Quick Launch URL as JSON.
res.json({ quickLaunchURL });
} catch (error) {
console.error("Error generating quick launch URL:", error);
res
.status(500)
.send("An error occurred while generating the quick launch URL.");
}
});
// -----------------------------------------------------------------------------
// Start the Express Server
// -----------------------------------------------------------------------------
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
/*
--------------------------------------------------------------------------------
Documentation for Advanced Configurables:
--------------------------------------------------------------------------------
1. OpenSSL Legacy Provider:
- The environment variable NODE_OPTIONS is set to "--openssl-legacy-provider" at
the top of this file. Adjust this if your environment requires a different setting.
2. S3 Configuration:
- The S3 region (S3_REGION) and bucket name (BUCKET_NAME) are defined in the
"Advanced Configurables" section. Change these values to match your deployment.
3. Serverless Packaging:
- The SERVERLESS_CMD constant defines the command used to package the garnished
code. If your packaging process changes or you need additional steps, update this
command accordingly.
4. CloudFormation Quick Launch:
- The Quick Launch URL is constructed based on the assumption that your CloudFormation
template is stored at a fixed location:
https://cloudfront-integration-bundles.s3.us-east-2.amazonaws.com/cloudformation.yaml
- The S3 uploads will be placed in a folder named "dist/<publicKey>/".
- The CloudFormation template itself is parameterized to use the dynamic publicKey for
function code locations.
5. Changing Placeholders:
- The placeholders "CROWDHANDLER_PUBLIC_KEY" and "CROWDHANDLER_API_DOMAIN" in the
fetched code are replaced with values provided via query parameters. Update the
replacement logic if additional placeholders are introduced.
This code is structured for public consumption with clear separation of concerns,
extensive inline documentation, and easy-to-modify advanced settings.
--------------------------------------------------------------------------------
*/