@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.
- package/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- 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
|
+
}
|