-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
327 lines (283 loc) · 10.4 KB
/
main.js
File metadata and controls
327 lines (283 loc) · 10.4 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
323
324
325
326
327
import { Cookies, SetCookie } from "cookies";
import { logger } from "log";
import {
generateSignature,
noCacheHeaders,
processQueryString,
queryStringParse,
sourceTokenandSignature,
} from "./helpers/misc_functions.js";
import { generateTokenObject } from "./helpers/cookie_generator.js";
import { getConfig } from "./helpers/config_get.js";
import {
checkoutBusterCheck,
roomsConfigCheck,
} from "./helpers/rooms_config.js";
import { signatureValidate } from "./helpers/signature_validation.js";
export function onClientResponse(request, response) {
try {
//Pull the cookie object from the middleman variable that we set in the onClientRequest function
let cookieValue = request.getVariable("PMUSER_CREDENTIALS");
let expires = request.getVariable("PMUSER_BUST");
if (cookieValue) {
let cookie = new SetCookie();
cookie.name = "crowdhandler";
cookie.path = "/";
cookie.value = encodeURIComponent(cookieValue);
if (expires === "true") {
cookie.expires = new Date(0);
}
response.addHeader("Set-Cookie", cookie.toHeader());
}
} catch (e) {
logger.error(`[CH] onClientResponse error: ${e.message || e}`);
// Response passes through without cookie — graceful degradation
}
}
export async function onClientRequest(request) {
try {
//UTC
const requestStartTime = new Date().getTime();
//Domain
const host = request.host;
const path = request.path;
//Environment Setup
const API_ENDPOINT = request.getVariable("PMUSER_CROWDHANDLER_API_ENDPOINT");
let PRIVATE_API_KEY;
let PUBLIC_API_KEY;
let HASHED_PRIVATE_API_KEY;
if (
request.getVariable("PMUSER_AUTOMATIONS") === "true" &&
host.includes("automations")
) {
PUBLIC_API_KEY = request.getVariable("PMUSER_CROWDHANDLER_SYNTH_KEY");
PRIVATE_API_KEY = request.getVariable("PMUSER_CROWDHANDLER_PV_SYNTH_KEY");
} else {
PUBLIC_API_KEY = request.getVariable("PMUSER_CROWDHANDLER_PUBLIC_KEY");
PRIVATE_API_KEY = request.getVariable("PMUSER_CROWDHANDLER_PRIVATE_KEY");
}
HASHED_PRIVATE_API_KEY = generateSignature(PRIVATE_API_KEY);
//Our function to return a response sending requests needing validation to the lite validator
function goToLiteValidator(targetURL, token, code) {
//get the user agent, lang and ip from the request object
let userAgent;
let lang;
let ip;
const ua = request.getHeader("User-Agent");
userAgent = ua ? encodeURIComponent(ua) : null;
const acceptLang = request.getHeader("Accept-Language");
lang = acceptLang ? encodeURIComponent(acceptLang) : null;
/**
* https://community.akamai.com/customers/s/question/0D54R00006rEQlqSAG/how-can-i-get-the-clientrequest-ip-in-onclientrequest?language=en_US
* Deprecated in favour of request.clientIp which became available 10/11/2024
try {
ip = encodeURIComponent(request.getVariable("PMUSER_CLIENT_IP"));
} catch (e) {
ip = "";
}
*/
ip = request.clientIp ? encodeURIComponent(request.clientIp) : null;
if (!token || token === "null" || token === "undefined") {
token = "";
}
if (!code) {
code = "";
}
let redirectLocation = {
Location: `/ch-api/v1/redirect/requests/${token}?url=${targetURL}&ch-public-key=${PUBLIC_API_KEY}&ch-code=${code}&whitelabel=true&agent=${userAgent}&lang=${lang}&ip=${ip}`,
};
let headers = Object.assign(noCacheHeaders, redirectLocation);
//Add a no-cache header here
request.respondWith(302, headers, "{}");
}
//Strip the URL of special CrowdHandler parameters
function generateCleanURL(queryString) {
let redirectLocation;
if (queryString) {
redirectLocation = { Location: `${path}${queryString}` };
} else {
redirectLocation = { Location: path };
}
let headers = Object.assign(noCacheHeaders, redirectLocation);
request.respondWith(302, headers, "{}");
}
/*
* Fetch the room config feed
* Method options are static, cache or edgekv.
*/
const integrationConfig = await getConfig(PUBLIC_API_KEY, "cache");
// Get the query string, parse and process it
const unprocessedQueryString = request.query;
let queryString;
//Parse query string
if (unprocessedQueryString) {
queryString = queryStringParse(unprocessedQueryString);
}
//Destructure special params from query string if they are present
let {
"ch-code": chCode,
"ch-id": chID,
"ch-id-signature": chIDSignature,
"ch-public-key": chPublicKey,
"ch-requested": chRequested,
} = queryString || {};
//Override chCode value if the current one is unusable
if (!chCode || chCode === "undefined" || chCode === "null") {
chCode = "";
}
// Process the query string
queryString = processQueryString(queryString);
//Do we need to bust the session on this request?
let bustCheckout = checkoutBusterCheck(
integrationConfig?.result || [],
host,
path + queryString
);
//Check the config file to see if we need to process this request any further
let validationRequired = roomsConfigCheck(
integrationConfig?.result || [],
host,
path + queryString
);
//Validate the signature.
if (validationRequired.status === true && bustCheckout === false) {
//URL encode the targetURL to be used later in redirects
let targetURL;
if (queryString) {
targetURL = encodeURIComponent(`https://${host}${path}${queryString}`);
} else {
targetURL = encodeURIComponent(`https://${host}${path}`);
}
//Cookie Jar
let cookies = new Cookies(request.getHeader("Cookie"));
let crowdhandlerCookieValue = cookies.get("crowdhandler");
if (crowdhandlerCookieValue) {
try {
crowdhandlerCookieValue = decodeURIComponent(crowdhandlerCookieValue);
crowdhandlerCookieValue = JSON.parse(crowdhandlerCookieValue);
} catch (e) {
logger.error('Malformed crowdhandler cookie');
crowdhandlerCookieValue = null;
}
}
//Determine where to source the token and signature from
let [token, signature] = sourceTokenandSignature(
chID,
chIDSignature,
crowdhandlerCookieValue
);
//Move to next stage of validation
if (token && signature) {
let validationResult = signatureValidate(
chRequested,
HASHED_PRIVATE_API_KEY,
token,
signature,
validationRequired.queueActivatesOn,
validationRequired.slug,
crowdhandlerCookieValue,
validationRequired.timeout,
validationRequired.patternType
);
if (validationResult.success !== true) {
logger.log(`[CH] ${host}${path} | validate | token:${token || 'none'}`);
goToLiteValidator(
targetURL,
token,
chCode,
validationResult.expiration
);
} else {
//If the signature is valid, we can continue to the next stage of the request
let freshToken;
//Flag if we're working with a never seen before signature
let newSignature;
let signatures = [];
let tokenObjects = [];
let requestStartTimeHash = generateSignature(
`${HASHED_PRIVATE_API_KEY}${requestStartTime}`
);
//Push tokens already in the cookie
if (crowdhandlerCookieValue?.tokens) {
for (const item of crowdhandlerCookieValue.tokens) {
tokenObjects.push(item);
}
}
//Determine if we're working with a new token or a previously seen one
if (
(Array.isArray(tokenObjects) && tokenObjects.length === 0) ||
(Array.isArray(tokenObjects) &&
tokenObjects[tokenObjects.length - 1].token !== token)
) {
freshToken = true;
} else {
freshToken = false;
//We want to work with the most recent array of signatures
for (const item of tokenObjects[tokenObjects.length - 1].signatures || []) {
signatures.push(item);
}
}
let tokenObject = new generateTokenObject(
requestStartTime,
requestStartTimeHash,
signature,
chRequested,
signatures,
token
);
if (
typeof signature === "string" &&
signatures.some((item) => item.sig === signature) === false
) {
signatures.push(tokenObject.signatureObject());
newSignature = true;
}
if (freshToken) {
//Reset the array. It's important we don't allow the PMUSER_CREDENTIALS variable exceed the byte limit.
tokenObjects = [];
tokenObjects.push(tokenObject.tokenObject());
} else {
//Update the cookie
tokenObjects[tokenObjects.length - 1].signatures = signatures;
tokenObjects[tokenObjects.length - 1].touched = requestStartTime;
tokenObjects[tokenObjects.length - 1].touchedSig =
requestStartTimeHash;
}
//Set the cookie in our middle man variable that we can access in onClientResponse
let newCookieValue = JSON.stringify({
integration: "akamai",
tokens: tokenObjects,
});
//We need to manage the bytesize of the cookie. If it's approaching 1024 bytes we're going to remove the oldest signatures from the cookie.
if (newCookieValue.length > 900) {
tokenObjects[tokenObjects.length - 1].signatures =
tokenObjects[tokenObjects.length - 1].signatures.slice(-1);
//Override with the sanitised tokenObjects
newCookieValue = JSON.stringify({
integration: "akamai",
tokens: tokenObjects,
});
}
request.setVariable("PMUSER_CREDENTIALS", newCookieValue);
//If we're coming in from the lite validator clean up the URL
if (newSignature) {
logger.log(`[CH] ${host}${path} | promoted | token:${token}`);
generateCleanURL(queryString);
} else {
logger.log(`[CH] ${host}${path} | allow | token:${token}`);
}
}
} else {
logger.log(`[CH] ${host}${path} | validate | token:${token || 'none'}`);
goToLiteValidator(targetURL, token, chCode, true);
}
} else if (bustCheckout === true) {
//If we need to bust the checkout, we need to clear the cookie
request.setVariable("PMUSER_CREDENTIALS", JSON.stringify({}));
request.setVariable("PMUSER_BUST", bustCheckout);
}
} catch (e) {
logger.error(`[CH] onClientRequest error: ${e.message || e}`);
// Return without respondWith() — Akamai passes to origin (fail open)
}
}