@mseep/anything-analyzer 3.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,1085 @@
1
+ import { EventEmitter } from "events";
2
+ import * as http from "http";
3
+ import * as https from "https";
4
+ import * as net from "net";
5
+ import { networkInterfaces } from "os";
6
+ import * as tls from "tls";
7
+ import * as url from "url";
8
+ import {
9
+ brotliDecompressSync,
10
+ gunzipSync,
11
+ inflateSync,
12
+ } from "zlib";
13
+ import { v4 as uuidv4 } from "uuid";
14
+ import { SocksClient } from "socks";
15
+ import type { CaManager } from "./ca-manager";
16
+ import type { ProxyConfig } from "../../shared/types";
17
+ import { generateCertPage, getCertFileContent, getCertDerContent, isCertDownloadHost } from "./cert-download-page";
18
+
19
+ const MAX_BODY_SIZE = 1024 * 1024; // 1MB — same limit as CdpManager
20
+ const BINARY_CONTENT_TYPES = [
21
+ "image/",
22
+ "font/",
23
+ "audio/",
24
+ "video/",
25
+ "application/octet-stream",
26
+ "application/pdf",
27
+ "application/zip",
28
+ ];
29
+ const STATIC_EXTENSIONS = /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot|map)$/i;
30
+
31
+ function headerToString(value: string | string[] | undefined): string {
32
+ return Array.isArray(value) ? value.join(",") : value || "";
33
+ }
34
+
35
+ function decodeCapturedBody(
36
+ body: Buffer,
37
+ contentEncoding: string | string[] | undefined,
38
+ ): Buffer {
39
+ const encodings = headerToString(contentEncoding)
40
+ .toLowerCase()
41
+ .split(",")
42
+ .map((encoding) => encoding.trim())
43
+ .filter(Boolean);
44
+
45
+ return encodings.reduceRight((decoded, encoding) => {
46
+ if (encoding === "br") return brotliDecompressSync(decoded);
47
+ if (encoding === "gzip" || encoding === "x-gzip") {
48
+ return gunzipSync(decoded);
49
+ }
50
+ if (encoding === "deflate") return inflateSync(decoded);
51
+ return decoded;
52
+ }, body);
53
+ }
54
+
55
+ function bodyToUtf8(
56
+ body: Buffer,
57
+ contentEncoding: string | string[] | undefined,
58
+ ): string {
59
+ try {
60
+ return decodeCapturedBody(body, contentEncoding)
61
+ .toString("utf-8")
62
+ .substring(0, MAX_BODY_SIZE);
63
+ } catch {
64
+ return body.toString("utf-8").substring(0, MAX_BODY_SIZE);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * MitmProxyServer — An embedded HTTP/HTTPS man-in-the-middle proxy.
70
+ *
71
+ * HTTP requests are forwarded directly (or via upstream proxy).
72
+ * HTTPS CONNECT requests are intercepted via dynamic TLS certificates
73
+ * issued by the CaManager's root CA.
74
+ *
75
+ * Supports upstream HTTP/HTTPS/SOCKS5 proxy for outbound connections.
76
+ *
77
+ * Emits 'response-captured' events with the same data shape as CdpManager,
78
+ * so CaptureEngine can handle them identically.
79
+ */
80
+ export class MitmProxyServer extends EventEmitter {
81
+ private server: http.Server | null = null;
82
+ private port: number | null = null;
83
+ private connections = new Set<net.Socket>();
84
+ private upstreamProxy: ProxyConfig | null = null;
85
+
86
+ constructor(private caManager: CaManager) {
87
+ super();
88
+ }
89
+
90
+ /**
91
+ * Set upstream proxy config. Pass null or { type: "none" } to disable.
92
+ */
93
+ setUpstreamProxy(config: ProxyConfig | null): void {
94
+ if (!config || config.type === "none") {
95
+ this.upstreamProxy = null;
96
+ console.log("[MitmProxy] Upstream proxy disabled");
97
+ } else {
98
+ this.upstreamProxy = config;
99
+ console.log(`[MitmProxy] Upstream proxy set to ${config.type}://${config.host}:${config.port}`);
100
+ }
101
+ }
102
+
103
+ async start(port: number): Promise<void> {
104
+ if (this.server) return;
105
+
106
+ this.server = http.createServer((req, res) => {
107
+ this.handleHttpRequest(req, res);
108
+ });
109
+
110
+ this.server.on("connect", (req, clientSocket, head) => {
111
+ this.handleConnect(req, clientSocket, head);
112
+ });
113
+
114
+ this.server.on("upgrade", (req, socket, head) => {
115
+ this.handleHttpWebSocketUpgrade(req, socket as net.Socket, head);
116
+ });
117
+
118
+ this.server.on("connection", (socket) => {
119
+ this.connections.add(socket);
120
+ socket.on("close", () => this.connections.delete(socket));
121
+ });
122
+
123
+ return new Promise((resolve, reject) => {
124
+ this.server!.listen(port, "0.0.0.0", () => {
125
+ this.port = port;
126
+ console.log(`[MitmProxy] Listening on port ${port}`);
127
+ resolve();
128
+ });
129
+ this.server!.on("error", (err) => {
130
+ console.error("[MitmProxy] Server error:", err.message);
131
+ reject(err);
132
+ });
133
+ });
134
+ }
135
+
136
+ async stop(): Promise<void> {
137
+ if (!this.server) return;
138
+
139
+ // Close all active connections
140
+ for (const socket of this.connections) {
141
+ socket.destroy();
142
+ }
143
+ this.connections.clear();
144
+
145
+ return new Promise((resolve) => {
146
+ this.server!.close(() => {
147
+ console.log("[MitmProxy] Stopped");
148
+ this.server = null;
149
+ this.port = null;
150
+ resolve();
151
+ });
152
+ });
153
+ }
154
+
155
+ isRunning(): boolean {
156
+ return this.server !== null && this.server.listening;
157
+ }
158
+
159
+ getPort(): number | null {
160
+ return this.port;
161
+ }
162
+
163
+ // ---- Upstream proxy helpers ----
164
+
165
+ /**
166
+ * Establish a TCP connection to the target, optionally through the upstream proxy.
167
+ * Returns a connected net.Socket ready for use.
168
+ */
169
+ private async connectToTarget(hostname: string, port: number): Promise<net.Socket> {
170
+ const proxy = this.upstreamProxy;
171
+
172
+ if (!proxy) {
173
+ // Direct connection
174
+ return new Promise((resolve, reject) => {
175
+ const socket = net.connect(port, hostname, () => resolve(socket));
176
+ socket.on("error", reject);
177
+ });
178
+ }
179
+
180
+ if (proxy.type === "socks5") {
181
+ return this.connectViaSocks5(hostname, port);
182
+ }
183
+
184
+ // HTTP/HTTPS upstream proxy — use CONNECT tunnel
185
+ return this.connectViaHttpProxy(hostname, port);
186
+ }
187
+
188
+ /**
189
+ * Establish a CONNECT tunnel through an HTTP/HTTPS upstream proxy.
190
+ * Uses tls.connect for HTTPS proxy type, net.connect for HTTP.
191
+ */
192
+ private connectViaHttpProxy(hostname: string, port: number): Promise<net.Socket> {
193
+ const proxy = this.upstreamProxy!;
194
+ const CONNECT_TIMEOUT = 30_000;
195
+
196
+ return new Promise((resolve, reject) => {
197
+ let settled = false;
198
+ const settle = (fn: () => void) => {
199
+ if (!settled) { settled = true; fn(); }
200
+ };
201
+
202
+ // Use tls.connect for HTTPS proxy, net.connect for HTTP
203
+ const connectFn = proxy.type === "https" ? tls.connect : net.connect;
204
+ const proxySocket = connectFn(proxy.port, proxy.host, () => {
205
+ // Build CONNECT request with optional auth
206
+ let connectReq = `CONNECT ${hostname}:${port} HTTP/1.1\r\nHost: ${hostname}:${port}\r\n`;
207
+ if (proxy.username && proxy.password) {
208
+ const auth = Buffer.from(`${proxy.username}:${proxy.password}`).toString("base64");
209
+ connectReq += `Proxy-Authorization: Basic ${auth}\r\n`;
210
+ }
211
+ connectReq += "\r\n";
212
+ proxySocket.write(connectReq);
213
+
214
+ // Wait for proxy response — accumulate raw Buffers to avoid encoding issues
215
+ const chunks: Buffer[] = [];
216
+ const HEADER_END = Buffer.from("\r\n\r\n");
217
+
218
+ const onData = (chunk: Buffer) => {
219
+ chunks.push(chunk);
220
+ const accumulated = Buffer.concat(chunks);
221
+ const endIdx = accumulated.indexOf(HEADER_END);
222
+ if (endIdx === -1) return; // Header not complete yet
223
+
224
+ proxySocket.removeListener("data", onData);
225
+
226
+ // Parse status line from ASCII-safe header portion
227
+ const headerStr = accumulated.subarray(0, endIdx).toString("ascii");
228
+ const statusLine = headerStr.split("\r\n")[0];
229
+ const statusCode = parseInt(statusLine.split(" ")[1], 10);
230
+
231
+ if (statusCode === 200) {
232
+ // Push back any trailing data (e.g. TLS ClientHello from server)
233
+ const trailing = accumulated.subarray(endIdx + 4);
234
+ if (trailing.length > 0) {
235
+ proxySocket.unshift(trailing);
236
+ }
237
+ settle(() => resolve(proxySocket));
238
+ } else {
239
+ proxySocket.destroy();
240
+ settle(() => reject(new Error(`Upstream proxy CONNECT failed: ${statusLine}`)));
241
+ }
242
+ };
243
+ proxySocket.on("data", onData);
244
+ });
245
+
246
+ // Timeout protection
247
+ const timer = setTimeout(() => {
248
+ proxySocket.destroy();
249
+ settle(() => reject(new Error(`Upstream proxy CONNECT timed out after ${CONNECT_TIMEOUT}ms`)));
250
+ }, CONNECT_TIMEOUT);
251
+
252
+ proxySocket.on("error", (err) => {
253
+ clearTimeout(timer);
254
+ settle(() => reject(new Error(`Upstream proxy connection failed: ${err.message}`)));
255
+ });
256
+
257
+ // Clear timeout on successful resolve
258
+ const origResolve = resolve;
259
+ resolve = ((val: net.Socket) => {
260
+ clearTimeout(timer);
261
+ origResolve(val);
262
+ }) as typeof resolve;
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Establish a connection through a SOCKS5 proxy.
268
+ */
269
+ private async connectViaSocks5(hostname: string, port: number): Promise<net.Socket> {
270
+ const proxy = this.upstreamProxy!;
271
+ const socksOptions: Parameters<typeof SocksClient.createConnection>[0] = {
272
+ proxy: {
273
+ host: proxy.host,
274
+ port: proxy.port,
275
+ type: 5,
276
+ ...(proxy.username && proxy.password
277
+ ? { userId: proxy.username, password: proxy.password }
278
+ : {}),
279
+ },
280
+ command: "connect",
281
+ destination: { host: hostname, port },
282
+ };
283
+
284
+ const { socket } = await SocksClient.createConnection(socksOptions);
285
+ return socket;
286
+ }
287
+
288
+ // ---- HTTP (non-CONNECT) proxy ----
289
+
290
+ /**
291
+ * Handle plain HTTP WebSocket upgrade (ws://) from the main server.
292
+ */
293
+ private handleHttpWebSocketUpgrade(
294
+ clientReq: http.IncomingMessage,
295
+ clientSocket: net.Socket,
296
+ head: Buffer,
297
+ ): void {
298
+ const startTime = Date.now();
299
+ const requestId = `proxy-${uuidv4()}`;
300
+ const targetUrl = clientReq.url || "/";
301
+ const parsed = url.parse(targetUrl);
302
+ const hostname = parsed.hostname || "localhost";
303
+ const port = parseInt(parsed.port || "80", 10);
304
+ const fullUrl = targetUrl.startsWith("http") ? targetUrl : `ws://${hostname}:${port}${parsed.path || "/"}`;
305
+
306
+ const connectToServer = (serverSocket: net.Socket): void => {
307
+ const wsHostHeader = port !== 80 ? `${hostname}:${port}` : hostname;
308
+ const headers = { ...clientReq.headers, host: wsHostHeader };
309
+ let rawReq = `${clientReq.method} ${parsed.path || "/"} HTTP/1.1\r\n`;
310
+ for (const [key, val] of Object.entries(headers)) {
311
+ if (val === undefined) continue;
312
+ const values = Array.isArray(val) ? val : [val];
313
+ for (const v of values) {
314
+ rawReq += `${key}: ${v}\r\n`;
315
+ }
316
+ }
317
+ rawReq += "\r\n";
318
+
319
+ serverSocket.write(rawReq);
320
+ if (head.length > 0) serverSocket.write(head);
321
+
322
+ let responseBuf = Buffer.alloc(0);
323
+ const onData = (chunk: Buffer): void => {
324
+ responseBuf = Buffer.concat([responseBuf, chunk]);
325
+ const headerEnd = responseBuf.indexOf("\r\n\r\n");
326
+ if (headerEnd === -1) return;
327
+
328
+ serverSocket.removeListener("data", onData);
329
+
330
+ const responseHeader = responseBuf.subarray(0, headerEnd + 4);
331
+ const trailing = responseBuf.subarray(headerEnd + 4);
332
+
333
+ const firstLine = responseHeader.toString("utf-8").split("\r\n")[0];
334
+ const statusCode = parseInt(firstLine.split(" ")[1], 10) || 101;
335
+
336
+ clientSocket.write(responseHeader);
337
+ if (trailing.length > 0) clientSocket.write(trailing);
338
+
339
+ serverSocket.pipe(clientSocket);
340
+ clientSocket.pipe(serverSocket);
341
+
342
+ serverSocket.on("error", () => clientSocket.destroy());
343
+ clientSocket.on("error", () => serverSocket.destroy());
344
+ serverSocket.on("close", () => clientSocket.destroy());
345
+ clientSocket.on("close", () => serverSocket.destroy());
346
+
347
+ this.emit("response-captured", {
348
+ requestId,
349
+ method: clientReq.method || "GET",
350
+ url: fullUrl,
351
+ requestHeaders: JSON.stringify(clientReq.headers || {}),
352
+ requestBody: null,
353
+ statusCode,
354
+ responseHeaders: JSON.stringify(this.parseRawHeaders(responseHeader.toString("utf-8"))),
355
+ responseBody: null,
356
+ contentType: null,
357
+ initiator: null,
358
+ durationMs: Date.now() - startTime,
359
+ isOptions: false,
360
+ isStatic: false,
361
+ isStreaming: false,
362
+ isWebSocket: true,
363
+ truncated: false,
364
+ timestamp: startTime,
365
+ });
366
+ };
367
+
368
+ serverSocket.on("data", onData);
369
+ };
370
+
371
+ if (this.upstreamProxy) {
372
+ this.connectToTarget(hostname, port)
373
+ .then(connectToServer)
374
+ .catch((err) => {
375
+ console.warn("[MitmProxy] WS upstream proxy error:", err.message);
376
+ clientSocket.destroy();
377
+ });
378
+ } else {
379
+ const serverSocket = net.connect(port, hostname, () => {
380
+ connectToServer(serverSocket);
381
+ });
382
+ serverSocket.on("error", (err) => {
383
+ console.warn(`[MitmProxy] WS connect error for ${hostname}:`, err.message);
384
+ clientSocket.destroy();
385
+ });
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Check if a request targets the proxy itself (direct browser access or
391
+ * proxy-configured client navigating to the proxy's own address).
392
+ * In both cases we serve the certificate download page.
393
+ */
394
+ private isSelfRequest(reqUrl: string, host: string): boolean {
395
+ // Case 1: Direct browser access (non-proxy request) — URL is a relative path
396
+ if (!reqUrl || (!reqUrl.startsWith("http://") && !reqUrl.startsWith("https://"))) {
397
+ return true;
398
+ }
399
+ // Case 2: Proxy client navigates to the proxy's own address
400
+ if (this.port !== null) {
401
+ const parsed = url.parse(reqUrl);
402
+ const targetPort = parseInt(parsed.port || "80", 10);
403
+ if (targetPort === this.port) {
404
+ const targetHost = (parsed.hostname || "").toLowerCase();
405
+ // Check common local identifiers
406
+ if (targetHost === "localhost" || targetHost === "127.0.0.1" || targetHost === "0.0.0.0") {
407
+ return true;
408
+ }
409
+ // Check against local network interfaces
410
+ const nets = networkInterfaces();
411
+ for (const name of Object.keys(nets)) {
412
+ for (const iface of nets[name] || []) {
413
+ if (iface.address === targetHost) return true;
414
+ }
415
+ }
416
+ }
417
+ }
418
+ return false;
419
+ }
420
+
421
+ private handleHttpRequest(
422
+ clientReq: http.IncomingMessage,
423
+ clientRes: http.ServerResponse,
424
+ ): void {
425
+ // Intercept cert download page requests
426
+ const host = (clientReq.headers.host || "").split(":")[0];
427
+ const targetUrl = clientReq.url || "";
428
+ const parsedTarget = url.parse(targetUrl);
429
+ const targetHost = (parsedTarget.hostname || "").split(":")[0];
430
+ if (isCertDownloadHost(host) || isCertDownloadHost(targetHost)) {
431
+ this.serveCertPage(clientReq, clientRes);
432
+ return;
433
+ }
434
+
435
+ // Direct browser access or self-referencing proxy request → serve cert page
436
+ if (this.isSelfRequest(targetUrl, host)) {
437
+ this.serveCertPage(clientReq, clientRes);
438
+ return;
439
+ }
440
+
441
+ const startTime = Date.now();
442
+ const requestId = `proxy-${uuidv4()}`;
443
+
444
+ if (!targetUrl) {
445
+ clientRes.writeHead(400);
446
+ clientRes.end("Bad Request");
447
+ return;
448
+ }
449
+
450
+ const parsed = parsedTarget;
451
+ const reqBodyChunks: Buffer[] = [];
452
+ let reqBodySize = 0;
453
+
454
+ clientReq.on("data", (chunk: Buffer) => {
455
+ if (reqBodySize < MAX_BODY_SIZE) {
456
+ reqBodyChunks.push(chunk);
457
+ }
458
+ reqBodySize += chunk.length;
459
+ });
460
+
461
+ clientReq.on("end", () => {
462
+ const reqBody = Buffer.concat(reqBodyChunks);
463
+ const headers = { ...clientReq.headers };
464
+
465
+ // Remove proxy-specific headers
466
+ delete headers["proxy-connection"];
467
+
468
+ const proxy = this.upstreamProxy;
469
+
470
+ let options: http.RequestOptions;
471
+ if (proxy && proxy.type !== "none" && proxy.type !== "socks5") {
472
+ // HTTP/HTTPS upstream proxy: send full URL to proxy
473
+ options = {
474
+ hostname: proxy.host,
475
+ port: proxy.port,
476
+ path: targetUrl, // Full URL as path when going through HTTP proxy
477
+ method: clientReq.method,
478
+ headers,
479
+ };
480
+ // Add proxy auth if configured
481
+ if (proxy.username && proxy.password) {
482
+ const auth = Buffer.from(`${proxy.username}:${proxy.password}`).toString("base64");
483
+ options.headers!["proxy-authorization"] = `Basic ${auth}`;
484
+ }
485
+ } else if (proxy && proxy.type === "socks5") {
486
+ // SOCKS5: connect to target through SOCKS, then send normal request
487
+ this.handleHttpViaSocks5(requestId, startTime, clientReq, clientRes, reqBody, targetUrl, parsed, headers);
488
+ return;
489
+ } else {
490
+ // Direct connection
491
+ options = {
492
+ hostname: parsed.hostname,
493
+ port: parsed.port || 80,
494
+ path: parsed.path,
495
+ method: clientReq.method,
496
+ headers,
497
+ };
498
+ }
499
+
500
+ const proxyReq = http.request(options, (proxyRes) => {
501
+ this.relayResponse(
502
+ requestId,
503
+ startTime,
504
+ clientReq,
505
+ reqBody,
506
+ targetUrl,
507
+ proxyRes,
508
+ clientRes,
509
+ );
510
+ });
511
+
512
+ proxyReq.on("error", (err) => {
513
+ console.warn("[MitmProxy] HTTP proxy error:", err.message);
514
+ if (!clientRes.headersSent) {
515
+ clientRes.writeHead(502);
516
+ clientRes.end("Bad Gateway");
517
+ }
518
+ });
519
+
520
+ if (reqBody.length > 0) proxyReq.write(reqBody);
521
+ proxyReq.end();
522
+ });
523
+ }
524
+
525
+ /**
526
+ * Handle HTTP request through SOCKS5 proxy — needs a custom socket.
527
+ */
528
+ private async handleHttpViaSocks5(
529
+ requestId: string,
530
+ startTime: number,
531
+ clientReq: http.IncomingMessage,
532
+ clientRes: http.ServerResponse,
533
+ reqBody: Buffer,
534
+ targetUrl: string,
535
+ parsed: url.UrlWithStringQuery,
536
+ headers: http.IncomingHttpHeaders,
537
+ ): Promise<void> {
538
+ try {
539
+ const socket = await this.connectViaSocks5(
540
+ parsed.hostname || "localhost",
541
+ parseInt(parsed.port || "80", 10),
542
+ );
543
+
544
+ const options: http.RequestOptions = {
545
+ hostname: parsed.hostname,
546
+ port: parsed.port || 80,
547
+ path: parsed.path,
548
+ method: clientReq.method,
549
+ headers,
550
+ createConnection: () => socket,
551
+ };
552
+
553
+ const proxyReq = http.request(options, (proxyRes) => {
554
+ this.relayResponse(requestId, startTime, clientReq, reqBody, targetUrl, proxyRes, clientRes);
555
+ });
556
+
557
+ proxyReq.on("error", (err) => {
558
+ console.warn("[MitmProxy] HTTP SOCKS5 proxy error:", err.message);
559
+ if (!clientRes.headersSent) {
560
+ clientRes.writeHead(502);
561
+ clientRes.end("Bad Gateway");
562
+ }
563
+ });
564
+
565
+ if (reqBody.length > 0) proxyReq.write(reqBody);
566
+ proxyReq.end();
567
+ } catch (err: unknown) {
568
+ const message = err instanceof Error ? err.message : String(err);
569
+ console.warn("[MitmProxy] SOCKS5 connection error:", message);
570
+ if (!clientRes.headersSent) {
571
+ clientRes.writeHead(502);
572
+ clientRes.end("Bad Gateway");
573
+ }
574
+ }
575
+ }
576
+
577
+ // ---- HTTPS CONNECT tunnel ----
578
+
579
+ private handleConnect(
580
+ req: http.IncomingMessage,
581
+ clientSocket: net.Socket,
582
+ head: Buffer,
583
+ ): void {
584
+ const [hostname, portStr] = (req.url || "").split(":");
585
+ const port = parseInt(portStr, 10) || 443;
586
+
587
+ if (!hostname) {
588
+ clientSocket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
589
+ return;
590
+ }
591
+
592
+ // Acknowledge CONNECT
593
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
594
+
595
+ // Create TLS server socket with a dynamic certificate for this host
596
+ const secureContext = this.caManager.getSecureContextForHost(hostname);
597
+ const tlsSocket = new tls.TLSSocket(clientSocket, {
598
+ isServer: true,
599
+ secureContext,
600
+ });
601
+
602
+ if (head.length > 0) tlsSocket.unshift(head);
603
+
604
+ // Create a mini HTTP server on the decrypted stream
605
+ const miniServer = http.createServer((decryptedReq, decryptedRes) => {
606
+ this.handleDecryptedRequest(
607
+ hostname,
608
+ port,
609
+ decryptedReq,
610
+ decryptedRes,
611
+ );
612
+ });
613
+
614
+ // Handle WebSocket upgrade requests inside the TLS tunnel
615
+ miniServer.on("upgrade", (upgradeReq, upgradeSocket, upgradeHead) => {
616
+ this.handleWebSocketUpgrade(hostname, port, upgradeReq, upgradeSocket as net.Socket, upgradeHead);
617
+ });
618
+
619
+ // Pipe the TLS socket into the mini server
620
+ miniServer.emit("connection", tlsSocket);
621
+
622
+ tlsSocket.on("error", (err) => {
623
+ console.warn(`[MitmProxy] TLS error for ${hostname}:`, err.message);
624
+ });
625
+
626
+ clientSocket.on("error", () => {
627
+ tlsSocket.destroy();
628
+ });
629
+ }
630
+
631
+ /**
632
+ * Handle a decrypted HTTPS request (after TLS interception).
633
+ * When upstream proxy is configured, establishes a tunnel first.
634
+ */
635
+ private handleDecryptedRequest(
636
+ hostname: string,
637
+ port: number,
638
+ clientReq: http.IncomingMessage,
639
+ clientRes: http.ServerResponse,
640
+ ): void {
641
+ // Serve cert download page over HTTPS too (LAN devices may access via HTTPS)
642
+ if (isCertDownloadHost(hostname)) {
643
+ this.serveCertPage(clientReq, clientRes);
644
+ return;
645
+ }
646
+
647
+ const startTime = Date.now();
648
+ const requestId = `proxy-${uuidv4()}`;
649
+ const fullUrl = `https://${hostname}${port !== 443 ? ":" + port : ""}${clientReq.url || "/"}`;
650
+
651
+ const reqBodyChunks: Buffer[] = [];
652
+ let reqBodySize = 0;
653
+
654
+ clientReq.on("data", (chunk: Buffer) => {
655
+ if (reqBodySize < MAX_BODY_SIZE) {
656
+ reqBodyChunks.push(chunk);
657
+ }
658
+ reqBodySize += chunk.length;
659
+ });
660
+
661
+ clientReq.on("end", () => {
662
+ const reqBody = Buffer.concat(reqBodyChunks);
663
+
664
+ if (this.upstreamProxy) {
665
+ // Route through upstream proxy
666
+ this.handleDecryptedViaProxy(
667
+ requestId, startTime, hostname, port, clientReq, clientRes, reqBody, fullUrl,
668
+ );
669
+ } else {
670
+ // Direct connection
671
+ this.handleDecryptedDirect(
672
+ requestId, startTime, hostname, port, clientReq, clientRes, reqBody, fullUrl,
673
+ );
674
+ }
675
+ });
676
+ }
677
+
678
+ /**
679
+ * Handle a WebSocket upgrade request received inside a decrypted TLS tunnel.
680
+ * Forwards the upgrade to the real server and pipes bidirectional frames.
681
+ */
682
+ private handleWebSocketUpgrade(
683
+ hostname: string,
684
+ port: number,
685
+ clientReq: http.IncomingMessage,
686
+ clientSocket: net.Socket,
687
+ head: Buffer,
688
+ ): void {
689
+ const startTime = Date.now();
690
+ const requestId = `proxy-${uuidv4()}`;
691
+ const fullUrl = `wss://${hostname}${port !== 443 ? ":" + port : ""}${clientReq.url || "/"}`;
692
+
693
+ const connectAndUpgrade = (targetSocket: net.Socket): void => {
694
+ // Perform TLS handshake with the real server
695
+ const tlsConnection = tls.connect(
696
+ {
697
+ host: hostname,
698
+ port,
699
+ socket: targetSocket,
700
+ rejectUnauthorized: false,
701
+ servername: hostname,
702
+ },
703
+ () => {
704
+ // Build the upgrade request to send to the real server
705
+ const wsHostHeader = port !== 443 ? `${hostname}:${port}` : hostname;
706
+ const headers = { ...clientReq.headers, host: wsHostHeader };
707
+ let rawReq = `${clientReq.method} ${clientReq.url} HTTP/1.1\r\n`;
708
+ for (const [key, val] of Object.entries(headers)) {
709
+ if (val === undefined) continue;
710
+ const values = Array.isArray(val) ? val : [val];
711
+ for (const v of values) {
712
+ rawReq += `${key}: ${v}\r\n`;
713
+ }
714
+ }
715
+ rawReq += "\r\n";
716
+
717
+ tlsConnection.write(rawReq);
718
+ if (head.length > 0) tlsConnection.write(head);
719
+
720
+ // Wait for the server's upgrade response
721
+ let responseBuf = Buffer.alloc(0);
722
+ const onData = (chunk: Buffer): void => {
723
+ responseBuf = Buffer.concat([responseBuf, chunk]);
724
+ const headerEnd = responseBuf.indexOf("\r\n\r\n");
725
+ if (headerEnd === -1) return;
726
+
727
+ tlsConnection.removeListener("data", onData);
728
+
729
+ const responseHeader = responseBuf.subarray(0, headerEnd + 4);
730
+ const trailing = responseBuf.subarray(headerEnd + 4);
731
+
732
+ // Parse status code from first line
733
+ const firstLine = responseHeader.toString("utf-8").split("\r\n")[0];
734
+ const statusCode = parseInt(firstLine.split(" ")[1], 10) || 101;
735
+
736
+ // Forward the server response header to the client
737
+ clientSocket.write(responseHeader);
738
+ if (trailing.length > 0) clientSocket.write(trailing);
739
+
740
+ // Pipe bidirectional WebSocket frames
741
+ tlsConnection.pipe(clientSocket);
742
+ clientSocket.pipe(tlsConnection);
743
+
744
+ tlsConnection.on("error", () => clientSocket.destroy());
745
+ clientSocket.on("error", () => tlsConnection.destroy());
746
+ tlsConnection.on("close", () => clientSocket.destroy());
747
+ clientSocket.on("close", () => tlsConnection.destroy());
748
+
749
+ // Emit capture event for the WebSocket upgrade request
750
+ this.emit("response-captured", {
751
+ requestId,
752
+ method: clientReq.method || "GET",
753
+ url: fullUrl,
754
+ requestHeaders: JSON.stringify(clientReq.headers || {}),
755
+ requestBody: null,
756
+ statusCode,
757
+ responseHeaders: JSON.stringify(this.parseRawHeaders(responseHeader.toString("utf-8"))),
758
+ responseBody: null,
759
+ contentType: null,
760
+ initiator: null,
761
+ durationMs: Date.now() - startTime,
762
+ isOptions: false,
763
+ isStatic: false,
764
+ isStreaming: false,
765
+ isWebSocket: true,
766
+ truncated: false,
767
+ timestamp: startTime,
768
+ });
769
+ };
770
+
771
+ tlsConnection.on("data", onData);
772
+ },
773
+ );
774
+
775
+ tlsConnection.on("error", (err) => {
776
+ console.warn(`[MitmProxy] WebSocket upstream TLS error for ${hostname}:`, err.message);
777
+ clientSocket.destroy();
778
+ });
779
+ };
780
+
781
+ if (this.upstreamProxy) {
782
+ this.connectToTarget(hostname, port)
783
+ .then(connectAndUpgrade)
784
+ .catch((err) => {
785
+ console.warn("[MitmProxy] WebSocket upstream proxy error:", err.message);
786
+ clientSocket.destroy();
787
+ });
788
+ } else {
789
+ const targetSocket = net.connect(port, hostname, () => {
790
+ connectAndUpgrade(targetSocket);
791
+ });
792
+ targetSocket.on("error", (err) => {
793
+ console.warn(`[MitmProxy] WebSocket connect error for ${hostname}:`, err.message);
794
+ clientSocket.destroy();
795
+ });
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Parse raw HTTP response headers into a key-value object.
801
+ */
802
+ private parseRawHeaders(raw: string): Record<string, string> {
803
+ const headers: Record<string, string> = {};
804
+ const lines = raw.split("\r\n").slice(1); // skip status line
805
+ for (const line of lines) {
806
+ if (!line) break;
807
+ const colonIdx = line.indexOf(":");
808
+ if (colonIdx > 0) {
809
+ const key = line.substring(0, colonIdx).trim().toLowerCase();
810
+ const value = line.substring(colonIdx + 1).trim();
811
+ headers[key] = value;
812
+ }
813
+ }
814
+ return headers;
815
+ }
816
+
817
+ /**
818
+ * Direct HTTPS request to the target (no upstream proxy).
819
+ */
820
+ private handleDecryptedDirect(
821
+ requestId: string,
822
+ startTime: number,
823
+ hostname: string,
824
+ port: number,
825
+ clientReq: http.IncomingMessage,
826
+ clientRes: http.ServerResponse,
827
+ reqBody: Buffer,
828
+ fullUrl: string,
829
+ ): void {
830
+ const hostHeader = port !== 443 ? `${hostname}:${port}` : hostname;
831
+ const options: https.RequestOptions = {
832
+ hostname,
833
+ port,
834
+ path: clientReq.url,
835
+ method: clientReq.method,
836
+ headers: { ...clientReq.headers, host: hostHeader },
837
+ rejectUnauthorized: false, // We are the MITM — upstream cert check is lax
838
+ };
839
+
840
+ const proxyReq = https.request(options, (proxyRes) => {
841
+ this.relayResponse(requestId, startTime, clientReq, reqBody, fullUrl, proxyRes, clientRes);
842
+ });
843
+
844
+ proxyReq.on("error", (err) => {
845
+ console.warn("[MitmProxy] HTTPS proxy error:", err.message);
846
+ if (!clientRes.headersSent) {
847
+ clientRes.writeHead(502);
848
+ clientRes.end("Bad Gateway");
849
+ }
850
+ });
851
+
852
+ if (reqBody.length > 0) proxyReq.write(reqBody);
853
+ proxyReq.end();
854
+ }
855
+
856
+ /**
857
+ * HTTPS request routed through the upstream proxy (HTTP/HTTPS/SOCKS5).
858
+ * Establishes a tunnel to the target, then performs TLS + HTTP on top.
859
+ */
860
+ private async handleDecryptedViaProxy(
861
+ requestId: string,
862
+ startTime: number,
863
+ hostname: string,
864
+ port: number,
865
+ clientReq: http.IncomingMessage,
866
+ clientRes: http.ServerResponse,
867
+ reqBody: Buffer,
868
+ fullUrl: string,
869
+ ): Promise<void> {
870
+ try {
871
+ const tunnelSocket = await this.connectToTarget(hostname, port);
872
+
873
+ const hostHeader = port !== 443 ? `${hostname}:${port}` : hostname;
874
+ const options: https.RequestOptions = {
875
+ hostname,
876
+ port,
877
+ path: clientReq.url,
878
+ method: clientReq.method,
879
+ headers: { ...clientReq.headers, host: hostHeader },
880
+ rejectUnauthorized: false,
881
+ socket: tunnelSocket, // Use the pre-established tunnel
882
+ };
883
+
884
+ const proxyReq = https.request(options, (proxyRes) => {
885
+ this.relayResponse(requestId, startTime, clientReq, reqBody, fullUrl, proxyRes, clientRes);
886
+ });
887
+
888
+ proxyReq.on("error", (err) => {
889
+ console.warn("[MitmProxy] HTTPS upstream proxy error:", err.message);
890
+ tunnelSocket.destroy();
891
+ if (!clientRes.headersSent) {
892
+ clientRes.writeHead(502);
893
+ clientRes.end("Bad Gateway");
894
+ }
895
+ });
896
+
897
+ if (reqBody.length > 0) proxyReq.write(reqBody);
898
+ proxyReq.end();
899
+ } catch (err: unknown) {
900
+ const message = err instanceof Error ? err.message : String(err);
901
+ console.warn("[MitmProxy] Upstream tunnel error:", message);
902
+ if (!clientRes.headersSent) {
903
+ clientRes.writeHead(502);
904
+ clientRes.end("Bad Gateway");
905
+ }
906
+ }
907
+ }
908
+
909
+ /**
910
+ * Relay upstream response back to the client, and emit a capture event.
911
+ */
912
+ private relayResponse(
913
+ requestId: string,
914
+ startTime: number,
915
+ clientReq: http.IncomingMessage,
916
+ reqBody: Buffer,
917
+ fullUrl: string,
918
+ proxyRes: http.IncomingMessage,
919
+ clientRes: http.ServerResponse,
920
+ ): void {
921
+ const resBodyChunks: Buffer[] = [];
922
+ let totalResSize = 0;
923
+ let truncated = false;
924
+
925
+ proxyRes.on("data", (chunk: Buffer) => {
926
+ if (totalResSize < MAX_BODY_SIZE) {
927
+ resBodyChunks.push(chunk);
928
+ } else {
929
+ truncated = true;
930
+ }
931
+ totalResSize += chunk.length;
932
+ });
933
+
934
+ proxyRes.on("end", () => {
935
+ const durationMs = Date.now() - startTime;
936
+ const resBody = Buffer.concat(resBodyChunks);
937
+ const contentType =
938
+ (proxyRes.headers["content-type"] as string) || null;
939
+ const method = clientReq.method || "GET";
940
+
941
+ // Determine if body should be captured (skip binary)
942
+ const isBinary = contentType
943
+ ? BINARY_CONTENT_TYPES.some((t) => contentType.startsWith(t))
944
+ : false;
945
+
946
+ const isStreaming =
947
+ contentType?.includes("text/event-stream") || false;
948
+ const isWebSocket = false; // Regular HTTP/HTTPS — WS upgrade is handled separately
949
+ const isOptions = method === "OPTIONS";
950
+ const isStatic = STATIC_EXTENSIONS.test(fullUrl);
951
+
952
+ const requestHeaders = JSON.stringify(clientReq.headers || {});
953
+ const responseHeaders = JSON.stringify(proxyRes.headers || {});
954
+
955
+ const requestBody =
956
+ reqBody.length > 0 && !isBinary
957
+ ? bodyToUtf8(reqBody, clientReq.headers["content-encoding"])
958
+ : null;
959
+
960
+ const responseBody =
961
+ resBody.length > 0 && !isBinary
962
+ ? bodyToUtf8(resBody, proxyRes.headers["content-encoding"])
963
+ : null;
964
+
965
+ this.emit("response-captured", {
966
+ requestId,
967
+ method,
968
+ url: fullUrl,
969
+ requestHeaders,
970
+ requestBody,
971
+ statusCode: proxyRes.statusCode || 0,
972
+ responseHeaders,
973
+ responseBody,
974
+ contentType,
975
+ initiator: null,
976
+ durationMs,
977
+ isOptions,
978
+ isStatic,
979
+ isStreaming,
980
+ isWebSocket,
981
+ truncated,
982
+ timestamp: startTime,
983
+ });
984
+ });
985
+
986
+ // Forward response to client
987
+ clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
988
+ proxyRes.pipe(clientRes);
989
+ }
990
+
991
+ /**
992
+ * Direct tunnel for WebSocket or other non-intercepted CONNECT targets.
993
+ * Routes through upstream proxy when configured.
994
+ */
995
+ private tunnelDirect(
996
+ hostname: string,
997
+ port: number,
998
+ clientSocket: net.Socket,
999
+ head: Buffer,
1000
+ ): void {
1001
+ if (this.upstreamProxy) {
1002
+ // Tunnel through upstream proxy
1003
+ this.connectToTarget(hostname, port)
1004
+ .then((serverSocket) => {
1005
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
1006
+ if (head.length > 0) serverSocket.write(head);
1007
+ serverSocket.pipe(clientSocket);
1008
+ clientSocket.pipe(serverSocket);
1009
+ serverSocket.on("error", () => clientSocket.destroy());
1010
+ clientSocket.on("error", () => serverSocket.destroy());
1011
+ })
1012
+ .catch((err) => {
1013
+ console.warn("[MitmProxy] Tunnel via upstream proxy error:", err.message);
1014
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
1015
+ });
1016
+ } else {
1017
+ const serverSocket = net.connect(port, hostname, () => {
1018
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
1019
+ if (head.length > 0) serverSocket.write(head);
1020
+ serverSocket.pipe(clientSocket);
1021
+ clientSocket.pipe(serverSocket);
1022
+ });
1023
+
1024
+ serverSocket.on("error", () => clientSocket.destroy());
1025
+ clientSocket.on("error", () => serverSocket.destroy());
1026
+ }
1027
+ }
1028
+
1029
+ // ---- Certificate download page ----
1030
+
1031
+ /**
1032
+ * Serve the certificate download page or the certificate file itself
1033
+ * when a client accesses the certificate hostnames.
1034
+ */
1035
+ private serveCertPage(
1036
+ req: http.IncomingMessage,
1037
+ res: http.ServerResponse,
1038
+ ): void {
1039
+ const reqPath = url.parse(req.url || "/").pathname || "/";
1040
+
1041
+ if (reqPath === "/cert.crt" || reqPath === "/cert.pem" || reqPath === "/cert.cer") {
1042
+ // Serve the CA certificate file for download
1043
+ try {
1044
+ // .cer → DER (binary) format for mobile compatibility
1045
+ // .crt / .pem → PEM (text) format
1046
+ const isDer = reqPath === "/cert.cer";
1047
+ const certContent = isDer
1048
+ ? getCertDerContent(this.caManager)
1049
+ : getCertFileContent(this.caManager);
1050
+ const contentType = isDer
1051
+ ? "application/x-x509-ca-cert"
1052
+ : "application/x-pem-file";
1053
+ const filename = isDer
1054
+ ? "anything-analyzer-ca.cer"
1055
+ : "anything-analyzer-ca.pem";
1056
+ res.writeHead(200, {
1057
+ "Content-Type": contentType,
1058
+ "Content-Disposition": `attachment; filename=\"${filename}\"`,
1059
+ "Content-Length": certContent.length,
1060
+ "Cache-Control": "no-cache",
1061
+ });
1062
+ res.end(certContent);
1063
+ } catch (err) {
1064
+ console.error("[MitmProxy] Failed to read CA cert:", err);
1065
+ res.writeHead(500);
1066
+ res.end("CA certificate not available. Please initialize the proxy first.");
1067
+ }
1068
+ return;
1069
+ }
1070
+
1071
+ // Serve the HTML download page.
1072
+ // Use the request's Host header for the download link so it works on
1073
+ // LAN devices that access the proxy directly by IP (not via cert.anything.test).
1074
+ const ua = req.headers["user-agent"] || "";
1075
+ const reqHost = req.headers.host || "";
1076
+ const html = isCertDownloadHost(reqHost.split(":")[0])
1077
+ ? generateCertPage(ua)
1078
+ : generateCertPage(ua, reqHost);
1079
+ res.writeHead(200, {
1080
+ "Content-Type": "text/html; charset=utf-8",
1081
+ "Cache-Control": "no-cache",
1082
+ });
1083
+ res.end(html);
1084
+ }
1085
+ }