|
| 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