Skip to content

Commit 643a7f1

Browse files
authored
Merge pull request #94 from GoogleCloudPlatform/breakup-actions-2
Exposing functions to independently apply actions pre-request and post-response
2 parents 66e6a38 + e946b22 commit 643a7f1

File tree

3 files changed

+188
-24
lines changed

3 files changed

+188
-24
lines changed

src/index.test.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { expect, test, vi, Mock } from "vitest";
2121

2222
import {
2323
applyActions,
24+
fetchActions,
2425
callCreateAssessment,
2526
callListFirewallPolicies,
2627
createPartialEventWithSiteInfo,
@@ -39,6 +40,8 @@ import {
3940
EdgeResponseInit,
4041
FirewallPolicy,
4142
Event,
43+
applyPreRequestActions,
44+
applyPostResponseActions,
4245
} from "./index";
4346

4447
import { FetchApiRequest, FetchApiResponse } from "./fetchApi";
@@ -83,7 +86,8 @@ class TestContext extends RecaptchaContext {
8386
log_messages: Array<[LogLevel, string[]]> = [];
8487

8588
constructor(config: RecaptchaConfig) {
86-
super(config);
89+
// Deep copy the config so it can be modified independently.
90+
super(JSON.parse(JSON.stringify(config)));
8791
}
8892

8993
createRequest(url: string, options: any): EdgeRequest {
@@ -344,6 +348,7 @@ test("ApplyActions-setHeader", async () => {
344348
expect(resp.text()).toEqual("<HTML>Hello World</HTML>");
345349
expect(fetch).toHaveBeenCalledTimes(1);
346350
});
351+
347352
test("ApplyActions-redirect", async () => {
348353
const context = new TestContext(testConfig);
349354
const req = new FetchApiRequest("https://www.example.com/originalreq");
@@ -1087,6 +1092,7 @@ test("processRequest-dump", async () => {
10871092
["debug", ["[rpc] listFirewallPolicies (ok)"]],
10881093
["debug", ["local assessment succeeded"]],
10891094
["debug", ["terminalAction: allow"]],
1095+
["debug", ["Applying response actions, ignoring request action"]],
10901096
],
10911097
exceptions: [],
10921098
create_assessment_headers: [],
@@ -1369,3 +1375,123 @@ test("DebugTrace-format", () => {
13691375
"list_firewall_policies_status=ok;policy_count=10;site_key_used=session;site_keys_present=asce;empty_config=apikey,endpoint;performance_counters=",
13701376
);
13711377
});
1378+
1379+
test("fetchActions-localAssessment", async () => {
1380+
const context = new TestContext(testConfig);
1381+
context.config.sessionJsInjectPath = "/teste2e;/another/path";
1382+
const req = new FetchApiRequest("https://www.example.com/teste2e");
1383+
const testPolicies = [
1384+
{
1385+
name: "test-policy",
1386+
description: "test-description",
1387+
path: "/teste2e",
1388+
actions: [{ block: {} }],
1389+
},
1390+
{
1391+
name: "test-policy2",
1392+
description: "test-description2",
1393+
path: "/teste2e",
1394+
actions: [{ redirect: {} }],
1395+
},
1396+
];
1397+
vi.stubGlobal("fetch", vi.fn());
1398+
(fetch as Mock).mockImplementationOnce(() =>
1399+
Promise.resolve({
1400+
status: 200,
1401+
headers: new Headers(),
1402+
json: () => Promise.resolve({ firewallPolicies: testPolicies }),
1403+
}),
1404+
);
1405+
const actions = await fetchActions(context, req);
1406+
expect(actions).toEqual([
1407+
{
1408+
injectjs: {},
1409+
},
1410+
{
1411+
block: {},
1412+
},
1413+
]);
1414+
expect(fetch).toHaveBeenCalledTimes(1);
1415+
});
1416+
1417+
test("fetchActions-createAssessment", async () => {
1418+
const context = new TestContext(testConfig);
1419+
const req = new FetchApiRequest("https://www.example.com/testlocal");
1420+
vi.stubGlobal("fetch", vi.fn());
1421+
(fetch as Mock)
1422+
.mockImplementationOnce((req) => {
1423+
expect(req.url).toEqual("https://recaptchaenterprise.googleapis.com/v1/projects/12345/fetchFirewallPolicies");
1424+
const testPolicies = [
1425+
{
1426+
name: "test-policy",
1427+
description: "test-description",
1428+
path: "/testlocal",
1429+
condition: "test-condition",
1430+
actions: [{ block: {} }],
1431+
},
1432+
];
1433+
Promise.resolve({
1434+
status: 200,
1435+
headers: new Headers(),
1436+
json: () => Promise.resolve({ firewallPolicies: testPolicies }),
1437+
});
1438+
})
1439+
.mockImplementationOnce((req) => {
1440+
expect(req.url).toEqual("https://recaptchaenterprise.googleapis.com/v1/projects/12345/assessments?key=abc123");
1441+
return Promise.resolve({
1442+
status: 200,
1443+
headers: new Headers(),
1444+
json: () =>
1445+
Promise.resolve({
1446+
name: "projects/12345/assessments/1234567890",
1447+
firewallPolicyAssessment: {
1448+
firewallPolicy: {
1449+
actions: [{ block: {} }],
1450+
},
1451+
},
1452+
}),
1453+
});
1454+
});
1455+
const actions = await fetchActions(context, req);
1456+
expect(fetch).toHaveBeenCalledTimes(2);
1457+
expect(actions).toEqual([
1458+
{
1459+
block: {},
1460+
},
1461+
]);
1462+
});
1463+
1464+
test("applyPreRequestActions - terminal", async () => {
1465+
const context = new TestContext(testConfig);
1466+
const req = new FetchApiRequest("https://www.example.com/teste2e");
1467+
const resp = (await applyPreRequestActions(context, req, [{ block: {} }])) as EdgeResponse;
1468+
expect(resp.status).toEqual(403);
1469+
});
1470+
1471+
test("applyPreRequestActions - non-terminal", async () => {
1472+
const context = new TestContext(testConfig);
1473+
const req = new FetchApiRequest("https://www.example.com/teste2e");
1474+
expect(req.getHeader("my-custom-header")).toBeNull();
1475+
const resp = await applyPreRequestActions(context, req, [
1476+
{
1477+
setHeader: {
1478+
key: "my-custom-header",
1479+
value: "test123",
1480+
},
1481+
},
1482+
]);
1483+
expect(resp).toBeNull();
1484+
expect(req.getHeader("my-custom-header")).toEqual("test123");
1485+
});
1486+
1487+
test("applyPostResponseActions", async () => {
1488+
const context = new TestContext(testConfig);
1489+
const inputResp: Promise<EdgeResponse> = Promise.resolve({
1490+
status: 200,
1491+
headers: new Headers(),
1492+
text: () => "<HTML>Hello World</HTML>",
1493+
});
1494+
1495+
const resp = await applyPostResponseActions(context, await inputResp, [{injectjs: {}}]);
1496+
expect(await resp.text()).toEqual('<HTML><script src="test.js"/>Hello World</HTML>');
1497+
});

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export { ListFirewallPoliciesResponse, callListFirewallPolicies } from "./listFi
3535

3636
export {
3737
applyActions,
38+
applyPreRequestActions,
39+
applyPostResponseActions,
40+
fetchActions,
3841
evaluatePolicyAssessment,
3942
localPolicyAssessment,
4043
policyConditionMatch,

src/policy.ts

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,24 @@ export async function evaluatePolicyAssessment(context: RecaptchaContext, req: E
145145
}
146146

147147
/**
148-
* Apply actions to a request.
148+
* Apply pre-request actions. If a terminal action is applied it will generate a response
149+
* which will be returned. Non terminal actions will modify the request and return null;
149150
*/
150-
export async function applyActions(
151+
export async function applyPreRequestActions(
151152
context: RecaptchaContext,
152153
req: EdgeRequest,
153154
actions: action.Action[],
154-
): Promise<EdgeResponse> {
155+
): Promise<EdgeResponse | null> {
155156
let terminalAction: action.Action = action.createAllowAction();
156157
const reqNonterminalActions: action.RequestNonTerminalAction[] = [];
157-
const respNonterminalActions: action.ResponseNonTerminalAction[] = [];
158158

159-
// Actions are assumed to be in order of processing. Non-terminal actions must
160-
// be processed before terminal actions, and will be ignored if erroniously
161-
// placed after terminal actions.
162-
filterActions: for (const action of actions) {
159+
for (const action of actions) {
163160
if (isTerminalAction(action)) {
164161
terminalAction = action;
165162
} else if (isRequestNonTerminalAction(action)) {
166163
reqNonterminalActions.push(action);
167164
} else if (isResponseNonTerminalAction(action)) {
168-
respNonterminalActions.push(action);
165+
context.log("debug", "Applying request actions, ignoring response actions");
169166
} else {
170167
/* v8 ignore next */
171168
throw new Error("Unsupported action: " + action);
@@ -206,23 +203,38 @@ export async function applyActions(
206203
context.log("debug", "reqNonterminal action: setHeader");
207204
if (isSetHeaderAction(action)) {
208205
req.addHeader(action.setHeader.key ?? "", action.setHeader.value ?? "");
209-
continue;
210-
}
211-
if (isSubstituteAction(action)) {
206+
} else if (isSubstituteAction(action)) {
212207
context.log("debug", "reqNonterminal action: substitute");
213208
const url = new URL(req.url);
214209
req.url = `${url.origin}${action.substitute.path}`;
215-
continue;
210+
} else {
211+
/* v8 ignore next 2 lines */
212+
throw new Error("Unsupported pre-request action: " + action);
216213
}
217-
/* v8 ignore next 2 lines */
218-
throw new Error("Unsupported pre-request action: " + action);
219214
}
220215

221216
context.log("debug", "terminalAction: allow");
222-
// Fetch from the backend, whether redirected or not.
223-
let resp = context.fetch_origin(req);
217+
return null;
218+
}
219+
220+
/**
221+
* Apply post response actions. Returns a (possibly modified) response.
222+
*/
223+
export async function applyPostResponseActions(context: RecaptchaContext, resp: EdgeResponse, actions: action.Action[]): Promise<EdgeResponse> {
224+
const respNonterminalActions: action.ResponseNonTerminalAction[] = [];
225+
for (const action of actions) {
226+
if (isTerminalAction(action) || isRequestNonTerminalAction(action)) {
227+
context.log("debug", "Applying response actions, ignoring request action");
228+
} else if (isResponseNonTerminalAction(action)) {
229+
respNonterminalActions.push(action);
230+
} else {
231+
/* v8 ignore next */
232+
throw new Error("Unsupported action: " + action);
233+
}
234+
}
224235

225236
// Handle Post-Response actions.
237+
let modifiedResp = resp;
226238
const once = new Set<string>();
227239
for (const action of respNonterminalActions) {
228240
if (isInjectJsAction(action)) {
@@ -232,24 +244,40 @@ export async function applyActions(
232244
once.add("injectjs");
233245
context.log("debug", "respNonterminal action: injectjs");
234246
context.log_performance_debug("[func] injectJS - start");
235-
resp = context.injectRecaptchaJs(await resp);
247+
modifiedResp = await context.injectRecaptchaJs(resp);
236248
// If 'debug' is enabled, await the response to get reasonable performance metrics.
237-
if(context.config.debug) {
238-
resp = Promise.resolve(await resp);
249+
if (context.config.debug) {
250+
modifiedResp = await Promise.resolve(resp);
239251
}
240252
context.log_performance_debug("[func] injectJS - end");
241253
} else {
242254
throw new Error("Unsupported post-response action: " + action);
243255
}
244256
}
257+
return modifiedResp;
258+
}
245259

246-
return resp;
260+
/**
261+
* Apply actions to a request.
262+
*/
263+
export async function applyActions(
264+
context: RecaptchaContext,
265+
req: EdgeRequest,
266+
actions: action.Action[],
267+
): Promise<EdgeResponse> {
268+
const response = await applyPreRequestActions(context, req, actions);
269+
if (response !== null) {
270+
return response;
271+
}
272+
let resp = await context.fetch_origin(req);
273+
return applyPostResponseActions(context, resp, actions);
247274
}
248275

249276
/**
250-
* Process reCAPTCHA request.
277+
*
278+
* Fetches a list of the applicable actions, given a request.
251279
*/
252-
export async function processRequest(context: RecaptchaContext, req: EdgeRequest): Promise<EdgeResponse> {
280+
export async function fetchActions(context: RecaptchaContext, req: EdgeRequest): Promise<action.Action[]> {
253281
let actions: action.Action[] = [];
254282
try {
255283
const localAssessment = await localPolicyAssessment(context, req);
@@ -279,7 +307,14 @@ export async function processRequest(context: RecaptchaContext, req: EdgeRequest
279307
}
280308
}
281309
}
310+
return actions;
311+
}
282312

313+
/**
314+
* Process reCAPTCHA request.
315+
*/
316+
export async function processRequest(context: RecaptchaContext, req: EdgeRequest): Promise<EdgeResponse> {
317+
const actions = await fetchActions(context, req);
283318
context.log_performance_debug("[func] applyActions - start");
284319
let resp = applyActions(context, req, actions);
285320
context.log_performance_debug("[func] applyActions - end");

0 commit comments

Comments
 (0)