Skip to content

Commit 7479c59

Browse files
committed
Refactor: use generic addHook/removeHook functions
1 parent 7bfd74f commit 7479c59

6 files changed

Lines changed: 237 additions & 53 deletions

File tree

docs/outbound-requests.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
# Monitoring Outbound Requests
22

3-
To monitor outbound HTTP/HTTPS requests made by your application, you can use the `onOutboundRequest` function. This is useful when you want to track external API calls, log outbound traffic, or analyze what domains your application connects to.
3+
To monitor outbound HTTP/HTTPS requests made by your application, you can use the `addHook` function with the `beforeOutboundRequest` hook. This is useful when you want to track external API calls, log outbound traffic, or analyze what domains your application connects to.
44

55
## Basic Usage
66

77
```js
8-
const { onOutboundRequest } = require("@aikidosec/firewall");
8+
const { addHook } = require("@aikidosec/firewall");
99

10-
onOutboundRequest(({ url, port, method }) => {
10+
addHook("beforeOutboundRequest", ({ url, port, method }) => {
1111
// url is a URL object: https://nodejs.org/api/url.html#class-url
1212
console.log(`${new Date().toISOString()} - ${method} ${url.href}`);
1313
});
1414
```
1515

16+
## Removing Hooks
17+
18+
You can remove a previously registered hook using the `removeHook` function:
19+
20+
```js
21+
const { addHook, removeHook } = require("@aikidosec/firewall");
22+
23+
function myHook({ url, port, method }) {
24+
console.log(`${method} ${url.href}`);
25+
}
26+
27+
addHook("beforeOutboundRequest", myHook);
28+
29+
// Later, when you want to remove it:
30+
removeHook("beforeOutboundRequest", myHook);
31+
```
32+
1633
## Important Notes
1734

18-
- You can register multiple callbacks by calling `onOutboundRequest` multiple times.
19-
- Callbacks are triggered for all HTTP/HTTPS requests made through Node.js built-in modules (`http`, `https`), builtin fetch function, undici and anything that uses that.
20-
- Callbacks are called when the connection is initiated, before knowing if Zen will block the request.
21-
- Errors thrown in callbacks (both sync and async) are silently caught and not logged to prevent breaking your application.
35+
- You can register multiple hooks by calling `addHook` multiple times.
36+
- The same hook function can only be registered once (duplicates are automatically prevented).
37+
- Hooks are triggered for all HTTP/HTTPS requests made through Node.js built-in modules (`http`, `https`), builtin fetch function, undici and anything that uses that.
38+
- Hooks are called when the connection is initiated, before knowing if Zen will block the request.
39+
- Errors thrown in hooks (both sync and async) are silently caught and not logged to prevent breaking your application.

library/agent/Agent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { wrapInstalledPackages } from "./wrapInstalledPackages";
2727
import { Wrapper } from "./Wrapper";
2828
import { isAikidoCI } from "../helpers/isAikidoCI";
2929
import { AttackLogger } from "./AttackLogger";
30-
import { triggerOutboundRequestHooks } from "./hooks/outboundRequest";
30+
import { executeHooks } from "./hooks";
3131
import { Packages } from "./Packages";
3232
import { AIStatistics } from "./AIStatistics";
3333
import { isNewInstrumentationUnitTest } from "../helpers/isNewInstrumentationUnitTest";
@@ -570,7 +570,7 @@ export class Agent {
570570
}
571571

572572
onConnectHTTP(url: URL, port: number, method: string) {
573-
triggerOutboundRequestHooks({ url, port, method });
573+
executeHooks("beforeOutboundRequest", { url, port, method });
574574
}
575575

576576
onRouteExecute(context: Context) {

library/agent/hooks.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import * as t from "tap";
2+
import { addHook, removeHook, executeHooks, OutboundRequestInfo } from "./hooks";
3+
4+
t.test("it works", async (t) => {
5+
let hookOneCalls = 0;
6+
let hookTwoCalls = 0;
7+
8+
const testRequest: OutboundRequestInfo = {
9+
url: new URL("https://example.com"),
10+
port: 443,
11+
method: "GET",
12+
};
13+
14+
function hook1(request: OutboundRequestInfo) {
15+
t.equal(request.url.href, "https://example.com/");
16+
t.equal(request.port, 443);
17+
t.equal(request.method, "GET");
18+
hookOneCalls++;
19+
}
20+
21+
function hook2(request: OutboundRequestInfo) {
22+
t.equal(request.url.href, "https://example.com/");
23+
t.equal(request.port, 443);
24+
t.equal(request.method, "GET");
25+
hookTwoCalls++;
26+
}
27+
28+
function hook3() {
29+
throw new Error("hook3 should not be called");
30+
}
31+
32+
t.same(hookOneCalls, 0, "hookOneCalls starts at 0");
33+
t.same(hookTwoCalls, 0, "hookTwoCalls starts at 0");
34+
35+
executeHooks("beforeOutboundRequest", testRequest);
36+
37+
t.same(hookOneCalls, 0, "hookOneCalls still at 0");
38+
t.same(hookTwoCalls, 0, "hookTwoCalls still at 0");
39+
40+
addHook("beforeOutboundRequest", hook1);
41+
// @ts-expect-error some other hook is not defined in the types
42+
addHook("someOtherHook", hook3);
43+
executeHooks("beforeOutboundRequest", testRequest);
44+
45+
t.equal(hookOneCalls, 1, "hook1 called once");
46+
t.equal(hookTwoCalls, 0, "hook2 not called");
47+
48+
addHook("beforeOutboundRequest", hook2);
49+
executeHooks("beforeOutboundRequest", testRequest);
50+
51+
t.equal(hookOneCalls, 2, "hook1 called twice");
52+
t.equal(hookTwoCalls, 1, "hook2 called once");
53+
54+
removeHook("beforeOutboundRequest", hook1);
55+
executeHooks("beforeOutboundRequest", testRequest);
56+
57+
t.equal(hookOneCalls, 2, "hook1 still called twice");
58+
t.equal(hookTwoCalls, 2, "hook2 called twice");
59+
60+
removeHook("beforeOutboundRequest", hook2);
61+
executeHooks("beforeOutboundRequest", testRequest);
62+
63+
t.equal(hookOneCalls, 2, "hook1 still called twice");
64+
t.equal(hookTwoCalls, 2, "hook2 still called twice");
65+
});
66+
67+
t.test("it handles errors gracefully", async (t) => {
68+
let successCalls = 0;
69+
70+
function throwingHook() {
71+
throw new Error("This should be caught");
72+
}
73+
74+
function successHook() {
75+
successCalls++;
76+
}
77+
78+
const testRequest: OutboundRequestInfo = {
79+
url: new URL("https://example.com"),
80+
port: 443,
81+
method: "POST",
82+
};
83+
84+
addHook("beforeOutboundRequest", throwingHook);
85+
addHook("beforeOutboundRequest", successHook);
86+
87+
// Should not throw even though one hook throws
88+
t.doesNotThrow(() => {
89+
executeHooks("beforeOutboundRequest", testRequest);
90+
});
91+
92+
t.equal(successCalls, 1, "success hook still called despite error in other hook");
93+
94+
removeHook("beforeOutboundRequest", throwingHook);
95+
removeHook("beforeOutboundRequest", successHook);
96+
});
97+
98+
t.test("it handles async hooks with rejected promises", async (t) => {
99+
let asyncCalls = 0;
100+
101+
async function asyncHook() {
102+
asyncCalls++;
103+
throw new Error("Async error");
104+
}
105+
106+
const testRequest: OutboundRequestInfo = {
107+
url: new URL("https://example.com"),
108+
port: 443,
109+
method: "DELETE",
110+
};
111+
112+
addHook("beforeOutboundRequest", asyncHook);
113+
114+
// Should not throw even though async hook rejects
115+
t.doesNotThrow(() => {
116+
executeHooks("beforeOutboundRequest", testRequest);
117+
});
118+
119+
t.equal(asyncCalls, 1, "async hook was called");
120+
121+
removeHook("beforeOutboundRequest", asyncHook);
122+
});
123+
124+
t.test("it prevents duplicate hooks using Set", async (t) => {
125+
let hookCalls = 0;
126+
127+
function hook() {
128+
hookCalls++;
129+
}
130+
131+
const testRequest: OutboundRequestInfo = {
132+
url: new URL("https://example.com"),
133+
port: 443,
134+
method: "GET",
135+
};
136+
137+
addHook("beforeOutboundRequest", hook);
138+
addHook("beforeOutboundRequest", hook); // Try to add the same hook again
139+
140+
executeHooks("beforeOutboundRequest", testRequest);
141+
142+
t.equal(hookCalls, 1, "hook only called once despite being added twice");
143+
144+
removeHook("beforeOutboundRequest", hook);
145+
});

library/agent/hooks.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export type OutboundRequestInfo = {
2+
url: URL;
3+
port: number;
4+
method: string;
5+
};
6+
7+
type HookName = "beforeOutboundRequest";
8+
9+
// Map hook names to argument types
10+
interface HookTypes {
11+
beforeOutboundRequest: {
12+
args: [data: OutboundRequestInfo];
13+
};
14+
}
15+
16+
const hooks = new Map<
17+
HookName,
18+
Set<(...args: HookTypes[HookName]["args"]) => void | Promise<void>>
19+
>();
20+
21+
export function addHook<N extends HookName>(
22+
name: N,
23+
fn: (...args: HookTypes[N]["args"]) => void | Promise<void>
24+
) {
25+
if (!hooks.has(name)) {
26+
hooks.set(name, new Set([fn]));
27+
} else {
28+
hooks.get(name)!.add(fn);
29+
}
30+
}
31+
32+
export function removeHook<N extends HookName>(
33+
name: N,
34+
fn: (...args: HookTypes[N]["args"]) => void | Promise<void>
35+
) {
36+
if (hooks.has(name)) {
37+
hooks.get(name)!.delete(fn);
38+
}
39+
}
40+
41+
export function executeHooks<N extends HookName>(
42+
name: N,
43+
...args: [...HookTypes[N]["args"]]
44+
): void {
45+
const hookSet = hooks.get(name);
46+
47+
for (const fn of hookSet ?? []) {
48+
try {
49+
const result = (fn as (...args: HookTypes[N]["args"]) => void | Promise<void>)(...args);
50+
// If it returns a promise, catch any errors but don't wait
51+
if (result instanceof Promise) {
52+
result.catch(() => {
53+
// Silently ignore errors from user hooks
54+
});
55+
}
56+
} catch {
57+
// Silently ignore errors from user hooks
58+
}
59+
}
60+
}

library/agent/hooks/outboundRequest.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

library/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { isESM } from "./helpers/isESM";
1515
import { checkIndexImportGuard } from "./helpers/indexImportGuard";
1616
import { setRateLimitGroup } from "./ratelimiting/group";
1717
import { isLibBundled } from "./helpers/isLibBundled";
18-
import { onOutboundRequest } from "./agent/hooks/outboundRequest";
18+
import { addHook, removeHook } from "./agent/hooks";
1919

2020
// Prevent logging twice / trying to start agent twice
2121
if (!isNewHookSystemUsed()) {
@@ -52,7 +52,8 @@ export {
5252
addKoaMiddleware,
5353
addRestifyMiddleware,
5454
setRateLimitGroup,
55-
onOutboundRequest,
55+
addHook,
56+
removeHook,
5657
};
5758

5859
// Required for ESM / TypeScript default export support
@@ -69,5 +70,6 @@ export default {
6970
addKoaMiddleware,
7071
addRestifyMiddleware,
7172
setRateLimitGroup,
72-
onOutboundRequest,
73+
addHook,
74+
removeHook,
7375
};

0 commit comments

Comments
 (0)