Skip to content

Commit f14521f

Browse files
committed
Add plugin upstream guide
1 parent edf3499 commit f14521f

2 files changed

Lines changed: 301 additions & 0 deletions

File tree

.vitepress/sidebars/guides.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ export const guidesSidebar: DefaultTheme.SidebarItem[] = [
230230
text: "Send Events to the Frontend",
231231
link: "/guides/events",
232232
},
233+
{
234+
text: "Intercept Requests and Connections",
235+
link: "/guides/plugin_upstream",
236+
},
233237
],
234238
},
235239
{

src/guides/plugin_upstream.md

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# Intercept Requests and Connections
2+
3+
The `onUpstream` hook allows backend plugins to intercept and modify HTTP requests before they are sent to their target. This enables powerful use cases such as adding authentication headers, routing requests through proxies, implementing custom authentication flows, and more.
4+
5+
::: info
6+
The `onUpstream` callback is called synchronously, so it's important to keep operations fast to avoid impacting overall performance.
7+
:::
8+
9+
## Understanding Upstream Plugins
10+
11+
Upstream plugins act similar to upstream proxies. They are configured as a set of rules in Caido's settings that map domains to plugins. For example, a rule might say "for domain `api.example.com`, call plugin `my-auth-plugin`". When a request matches a configured domain, Caido checks if the specified plugin has an `onUpstream` hook, and if so, invokes it synchronously before the request is sent.
12+
13+
The callback receives a `RequestSpecRaw` object representing the request that's about to be sent. You can:
14+
15+
- Modify the request (headers, URL, body, etc.)
16+
- Provide a different connection to use
17+
- Return `undefined` to let other plugins handle the request
18+
19+
::: tip
20+
We recommend including upstream plugin rule management in your plugin's UI to simplify setup for users. This allows users to configure which domains your plugin should handle directly from your plugin's interface.
21+
:::
22+
23+
## Registering the Upstream Callback
24+
25+
To intercept requests, register your callback using `sdk.events.onUpstream()` in your plugin's initialization function:
26+
27+
```ts
28+
import type { DefineAPI, SDK } from "caido:plugin";
29+
import type { RequestSpecRaw } from "caido:utils";
30+
31+
export type API = DefineAPI<{}>;
32+
33+
export function init(sdk: SDK<API>) {
34+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
35+
// Your upstream logic here
36+
});
37+
}
38+
```
39+
40+
The callback receives two parameters:
41+
42+
- `sdk`: The SDK instance (same as the one passed to `init`)
43+
- `request`: A `RequestSpecRaw` object representing the request to be sent
44+
45+
## Return Values
46+
47+
Your callback can return different values to control how the request is handled:
48+
49+
- **`undefined`**: Let other upstream plugins handle the request (or proceed normally if no other plugins match)
50+
- **`Connection`**: Use the provided connection to send the request
51+
- **`RequestSpec`**: Use the modified request instead of the original
52+
- **`{ connection?, request? }`**: Provide both a connection and/or a modified request
53+
54+
## Modifying Requests
55+
56+
You can modify requests by converting the `RequestSpecRaw` to a `RequestSpec` using `toSpec()`, making your changes, and returning the modified request.
57+
58+
### Adding Headers
59+
60+
Add custom headers to requests before they're sent:
61+
62+
```ts
63+
import type { DefineAPI, SDK } from "caido:plugin";
64+
import type { RequestSpecRaw } from "caido:utils";
65+
import { RequestSpec } from "caido:utils";
66+
67+
export type API = DefineAPI<{}>;
68+
69+
export function init(sdk: SDK<API>) {
70+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
71+
const spec = request.toSpec();
72+
spec.setHeader("X-Custom-Header", "custom-value");
73+
spec.setHeader("Authorization", "Bearer token123");
74+
return spec;
75+
});
76+
}
77+
```
78+
79+
### Changing Request Properties
80+
81+
Modify the URL, method, body, or other request properties:
82+
83+
```ts
84+
import type { DefineAPI, SDK } from "caido:plugin";
85+
import type { RequestSpecRaw } from "caido:utils";
86+
import { RequestSpec } from "caido:utils";
87+
88+
export type API = DefineAPI<{}>;
89+
90+
export function init(sdk: SDK<API>) {
91+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
92+
const spec = request.toSpec();
93+
94+
// Change the target URL
95+
spec.setHost("proxy.example.com");
96+
spec.setPort(8080);
97+
spec.setTls(false);
98+
99+
// Modify the method
100+
spec.setMethod("POST");
101+
102+
// Change the body
103+
spec.setBody("Modified request body");
104+
105+
return spec;
106+
});
107+
}
108+
```
109+
110+
::: warning
111+
At the moment, modifying the host/port will **NOT** modify how the connection is opened by Caido.
112+
If you need to change the target host of the connection, use `sdk.net.connect`.
113+
:::
114+
115+
## Modifying Connections
116+
117+
You can provide a custom connection that will be used to send the request. This is useful for routing requests through proxies or reusing existing connections.
118+
119+
### Routing Through a Proxy
120+
121+
Create a new connection to a proxy server:
122+
123+
```ts
124+
import type { DefineAPI, SDK } from "caido:plugin";
125+
import type { RequestSpecRaw } from "caido:utils";
126+
127+
export type API = DefineAPI<{}>;
128+
129+
export function init(sdk: SDK<API>) {
130+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
131+
// Create a connection to a proxy server
132+
const proxyConnection = await sdk.net.connect("https://proxy.example.com:8080");
133+
134+
return {
135+
connection: proxyConnection,
136+
request: request.toSpec(), // Optionally modify the request too
137+
};
138+
});
139+
}
140+
```
141+
142+
## Examples
143+
144+
### Implementing NTLM Authentication
145+
146+
Complex authentication flows like NTLM require multiple request-response exchanges. You can use `sdk.requests.send()` within your upstream callback to perform these exchanges, then return the authenticated connection and modified request.
147+
148+
This example demonstrates how to implement NTLM authentication by performing the three-message handshake:
149+
150+
```ts
151+
import type { DefineAPI, SDK } from "caido:plugin";
152+
import type { RequestSpecRaw } from "caido:utils";
153+
import { RequestSpec } from "caido:utils";
154+
155+
export type API = DefineAPI<{}>;
156+
157+
const credentials = {
158+
username: "user",
159+
password: "pass",
160+
domain: "DOMAIN",
161+
};
162+
163+
export function init(sdk: SDK<API>) {
164+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
165+
const domain = request.getHost();
166+
167+
// Check if this domain requires NTLM authentication
168+
if (!domain.includes("example.com")) {
169+
return undefined; // Let other plugins handle it
170+
}
171+
172+
try {
173+
const spec = request.toSpec();
174+
175+
// Step 1: Send Type 1 message (negotiate)
176+
const type1Message = createType1Message("", "");
177+
spec.setHeader("Authorization", type1Message);
178+
spec.setHeader("Connection", "keep-alive");
179+
180+
const { response, connection } = await sdk.requests.send(spec, {
181+
save: false,
182+
// plugins defaults to false when called from onUpstream, preventing recursion
183+
});
184+
185+
// Step 2: Extract Type 2 message (challenge)
186+
const wwwAuthenticate = response.getHeader("WWW-Authenticate")?.[0];
187+
if (!wwwAuthenticate) {
188+
return undefined;
189+
}
190+
191+
const type2Message = decodeType2Message(wwwAuthenticate);
192+
193+
// Step 3: Create Type 3 message (authenticate)
194+
const type3Message = createType3Message(
195+
type2Message,
196+
credentials.username,
197+
credentials.password,
198+
);
199+
200+
// Return the authenticated connection and request
201+
const finalSpec = request.toSpec();
202+
finalSpec.setHeader("Authorization", type3Message);
203+
finalSpec.setHeader("Connection", "Close");
204+
205+
return {
206+
connection,
207+
request: finalSpec,
208+
};
209+
} catch (error) {
210+
sdk.console.error("NTLM authentication failed:", error);
211+
return undefined;
212+
}
213+
});
214+
}
215+
```
216+
217+
::: warning
218+
When calling `sdk.requests.send()` from within an `onUpstream` callback, the `plugins` option defaults to `false` to prevent recursion. This ensures your helper request doesn't trigger upstream plugins again. However, when calling `sdk.requests.send()` from other functions (not within `onUpstream`), `plugins` defaults to `true`.
219+
:::
220+
221+
### Routing Requests to Different Servers
222+
223+
Route requests to different servers based on domain or other criteria:
224+
225+
```ts
226+
import type { DefineAPI, SDK } from "caido:plugin";
227+
import type { RequestSpecRaw } from "caido:utils";
228+
import { RequestSpec } from "caido:utils";
229+
230+
export type API = DefineAPI<{}>;
231+
232+
const routingRules: Record<string, string> = {
233+
"api.example.com": "https://api-backup.example.com",
234+
"cdn.example.com": "https://cdn-mirror.example.com",
235+
};
236+
237+
export function init(sdk: SDK<API>) {
238+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
239+
const originalHost = request.getHost();
240+
const targetHost = routingRules[originalHost];
241+
242+
if (!targetHost) {
243+
return undefined; // No routing rule, proceed normally
244+
}
245+
246+
// Create connection to the target server
247+
const connection = await sdk.net.connect(targetHost);
248+
249+
// Modify the request to use the new host
250+
const spec = request.toSpec();
251+
spec.setHost(new URL(targetHost).hostname);
252+
spec.setPort(parseInt(new URL(targetHost).port) || (targetHost.startsWith("https") ? 443 : 80));
253+
spec.setTls(targetHost.startsWith("https"));
254+
255+
return {
256+
connection,
257+
request: spec,
258+
};
259+
});
260+
}
261+
```
262+
263+
### Logging and Monitoring Requests
264+
265+
Log requests before they're sent for monitoring or debugging purposes:
266+
267+
```ts
268+
import type { DefineAPI, SDK } from "caido:plugin";
269+
import type { RequestSpecRaw } from "caido:utils";
270+
271+
export type API = DefineAPI<{}>;
272+
273+
export function init(sdk: SDK<API>) {
274+
sdk.events.onUpstream(async (sdk, request: RequestSpecRaw) => {
275+
const spec = request.toSpec();
276+
const url = spec.getUrl();
277+
const method = spec.getMethod();
278+
const headers = spec.getHeaders();
279+
280+
sdk.console.log(`[Upstream] ${method} ${url}`);
281+
sdk.console.log(`[Upstream] Headers: ${JSON.stringify(headers)}`);
282+
283+
// Return undefined to proceed normally after logging
284+
return undefined;
285+
});
286+
}
287+
```
288+
289+
## Plugin Ordering and Multiple Plugins
290+
291+
When multiple upstream plugins are enabled for the same domain, they are called in the order configured by the user in Caido's upstream plugin settings. If your plugin returns `undefined`, the next plugin in the chain will be tried. If all plugins return `undefined`, the request proceeds normally.
292+
293+
This allows multiple plugins to work together:
294+
295+
- A logging plugin can log requests and return `undefined`
296+
- An authentication plugin can add auth headers and return `undefined`
297+
- A routing plugin can change the target server

0 commit comments

Comments
 (0)