@jshookmcp/jshook 0.2.8 → 0.2.9
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/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-BNk-EoBt.mjs} +3 -3
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-Cq8q01kp.mjs} +5 -5
- package/dist/ConsoleMonitor-CPVQW1Y-.mjs +2201 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-BNPxu0RH.mjs} +1 -1
- package/dist/DetailedDataManager-BQQcxh64.mjs +217 -0
- package/dist/EventBus-DgPmwpeu.mjs +141 -0
- package/dist/EvidenceGraphBridge-SFesNera.mjs +153 -0
- package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-CWYgw0YW.mjs} +13 -6
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-gzWtkKuf.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-B9gZCdFP.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-BLDH0dCv.mjs} +4 -4
- package/dist/HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs +639 -0
- package/dist/InstrumentationSession-CvPC7Jwy.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CbVdCIJF.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-BsDZbLYm.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-Bcpml6II.mjs} +44 -18
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-dZtA1ZGn.mjs} +14 -53
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-B-FjA2mJ.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-D1lzJ_VG.mjs} +2 -2
- package/dist/PageController-Bqm2kZ_X.mjs +417 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-BOhyVsjx.mjs} +4 -4
- package/dist/PrerequisiteError-Dl33Svkz.mjs +20 -0
- package/dist/ResponseBuilder-D3iFYx2N.mjs +143 -0
- package/dist/ReverseEvidenceGraph-Dlsk94LC.mjs +269 -0
- package/dist/ScriptManager-aHHq0X7U.mjs +3000 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-CqdIFlQl.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-DhFaPvRO.mjs} +3 -3
- package/dist/ToolCatalog-C0JGZoOm.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-oC7aPrkv.mjs +45 -0
- package/dist/ToolRegistry-BjaF4oNz.mjs +131 -0
- package/dist/ToolRouter.policy-BWV67ZK-.mjs +304 -0
- package/dist/TraceRecorder-DgxyVbdQ.mjs +519 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-CePkipZY.mjs} +1 -1
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-BvKs-gxc.mjs} +2 -2
- package/dist/WorkflowEngine-CuvkZtWu.mjs +598 -0
- package/dist/analysis-CL9uACt9.mjs +463 -0
- package/dist/antidebug-CqDTB_uk.mjs +1081 -0
- package/dist/artifactRetention-CFEprwPw.mjs +591 -0
- package/dist/artifacts-Bk2-_uPq.mjs +59 -0
- package/dist/betterSqlite3-0pqusHHH.mjs +74 -0
- package/dist/binary-instrument-CXfpx6fT.mjs +979 -0
- package/dist/bind-helpers-xFfRF-qm.mjs +22 -0
- package/dist/boringssl-inspector-BH2D3VKc.mjs +180 -0
- package/dist/browser-BpOr5PEx.mjs +4082 -0
- package/dist/concurrency-Bt0yv1kJ.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-B0OANIBL.mjs} +88 -46
- package/dist/coordination-qUbyF8KU.mjs +259 -0
- package/dist/debugger-gnKxRSN0.mjs +1271 -0
- package/dist/definitions-6M-eejaT.mjs +53 -0
- package/dist/definitions-B18eyf0B.mjs +18 -0
- package/dist/definitions-B3QdlrHv.mjs +34 -0
- package/dist/definitions-B4rAvHNZ.mjs +63 -0
- package/dist/definitions-BB_4jnmy.mjs +37 -0
- package/dist/definitions-BMfYXoNC.mjs +43 -0
- package/dist/definitions-Beid2EB3.mjs +27 -0
- package/dist/definitions-C1UvM5Iy.mjs +126 -0
- package/dist/definitions-CXEI7QC72.mjs +216 -0
- package/dist/definitions-C_4r7Fo-2.mjs +14 -0
- package/dist/definitions-CkFDALoa.mjs +26 -0
- package/dist/definitions-Cke7zEb8.mjs +94 -0
- package/dist/definitions-ClJLzsJQ.mjs +25 -0
- package/dist/definitions-Cq-zroAU.mjs +28 -0
- package/dist/definitions-Cy3Sl6gV.mjs +34 -0
- package/dist/definitions-D3VsGcvz.mjs +47 -0
- package/dist/definitions-DVGfrn7y.mjs +96 -0
- package/dist/definitions-LKpC3-nL.mjs +9 -0
- package/dist/definitions-bAhHQJq9.mjs +359 -0
- package/dist/encoding-Bvz5jLRv.mjs +1065 -0
- package/dist/evidence-graph-bridge-C_fv9PuC.mjs +135 -0
- package/dist/{factory-CibqTNC8.mjs → factory-DxlGh9Xf.mjs} +37 -52
- package/dist/graphql-DYWzJ29s.mjs +1026 -0
- package/dist/handlers-9sAbfIg-.mjs +2552 -0
- package/dist/handlers-Bl8zkwz1.mjs +2716 -0
- package/dist/handlers-C67ktuRN.mjs +710 -0
- package/dist/handlers-C87g8oCe.mjs +276 -0
- package/dist/handlers-CTsDAO6p.mjs +681 -0
- package/dist/handlers-Cgyg6c0U.mjs +645 -0
- package/dist/handlers-D6j6yka7.mjs +2124 -0
- package/dist/handlers-DdFzXLvF.mjs +446 -0
- package/dist/handlers-DeLOCd5m.mjs +799 -0
- package/dist/handlers-DlCJN4Td.mjs +757 -0
- package/dist/handlers-DxGIq15_2.mjs +917 -0
- package/dist/handlers-U6L4xhuF.mjs +585 -0
- package/dist/handlers-tB9Mp9ZK.mjs +84 -0
- package/dist/handlers-tiy7EIBp.mjs +572 -0
- package/dist/handlers.impl-DS0d9fUw.mjs +761 -0
- package/dist/hooks-CzCWByww.mjs +898 -0
- package/dist/index.mjs +377 -155
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-P7ePRXQC.mjs +830 -0
- package/dist/manifest-2ToTpjv8.mjs +106 -0
- package/dist/manifest-3g71z6Bg.mjs +79 -0
- package/dist/manifest-82baTv4U.mjs +45 -0
- package/dist/manifest-B3QVVeBS.mjs +82 -0
- package/dist/manifest-BB2J8IMJ.mjs +149 -0
- package/dist/manifest-BKbgbSiY.mjs +60 -0
- package/dist/manifest-Bcf-TJzH.mjs +848 -0
- package/dist/manifest-BmtZzQiQ2.mjs +45 -0
- package/dist/manifest-Bnd7kqEY.mjs +55 -0
- package/dist/manifest-BqQX6OQC2.mjs +65 -0
- package/dist/manifest-BqrQ4Tpj.mjs +81 -0
- package/dist/manifest-Br4RPFt5.mjs +370 -0
- package/dist/manifest-C5qDjysN.mjs +107 -0
- package/dist/manifest-C9RT5nk32.mjs +34 -0
- package/dist/manifest-CAhOuvSl.mjs +204 -0
- package/dist/manifest-CBYWCUBJ.mjs +51 -0
- package/dist/manifest-CFADCRa1.mjs +37 -0
- package/dist/manifest-CQVhavRF.mjs +114 -0
- package/dist/manifest-CT7zZBV1.mjs +48 -0
- package/dist/manifest-CV12bcrF.mjs +121 -0
- package/dist/manifest-CXsRWjjI.mjs +224 -0
- package/dist/manifest-CZLUCfG02.mjs +95 -0
- package/dist/manifest-D6phHKFd.mjs +131 -0
- package/dist/manifest-DCyjf4n2.mjs +294 -0
- package/dist/manifest-DHsnKgP6.mjs +60 -0
- package/dist/manifest-Df_dliIe.mjs +55 -0
- package/dist/manifest-Dh8WBmEW.mjs +129 -0
- package/dist/manifest-DhKRAT8_.mjs +92 -0
- package/dist/manifest-DlpTj4ic2.mjs +193 -0
- package/dist/manifest-DrbmZcFl2.mjs +253 -0
- package/dist/manifest-DuwHjUa5.mjs +70 -0
- package/dist/manifest-DzwvxPJX.mjs +38 -0
- package/dist/manifest-NXctwWQq.mjs +68 -0
- package/dist/manifest-Sc_0JQ13.mjs +418 -0
- package/dist/manifest-gZ4s_UtG.mjs +96 -0
- package/dist/manifest-qSleDqdO.mjs +1023 -0
- package/dist/modules-C184v-S9.mjs +11365 -0
- package/dist/mojo-ipc-B_H61Afw.mjs +525 -0
- package/dist/network-671Cw6hV.mjs +3346 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-B1uGmrWZ.mjs} +219 -212
- package/dist/parse-args-BlRjqlkL.mjs +39 -0
- package/dist/platform-WmNn8Sxb.mjs +2070 -0
- package/dist/process-QcbIy5Zq.mjs +1401 -0
- package/dist/proxy-DqNs0bAd.mjs +170 -0
- package/dist/registry-D-6e18lB.mjs +34 -0
- package/dist/response-BQVP-xUn.mjs +28 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-DV-dpHFJ.mjs +586 -0
- package/dist/sourcemap-Dq8ez8vS.mjs +650 -0
- package/dist/ssrf-policy-ZaUfvhq7.mjs +166 -0
- package/dist/streaming-BUQ0VJsg.mjs +725 -0
- package/dist/tool-builder-DCbIC5Eo.mjs +186 -0
- package/dist/transform-CiYJfNX0.mjs +1007 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-DQTnHDs4.mjs +531 -0
- package/dist/workflow-f3xJOcjx.mjs +725 -0
- package/package.json +16 -16
- package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
- package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-CDe5WPSV.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-Bo4T3bz8.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-CwVLVdDM.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-DVkj9kpI.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-CPhOReNX.mjs} +1 -1
|
@@ -0,0 +1,3346 @@
|
|
|
1
|
+
import { t as logger } from "./logger-Dh_xb7_2.mjs";
|
|
2
|
+
import { Gt as NETWORK_REPLAY_MAX_REDIRECTS, Wt as NETWORK_HAR_BODY_CONCURRENCY, dt as ICMP_DEFAULT_PACKET_SIZE, ft as ICMP_PROBE_TIMEOUT_MS, pt as ICMP_TRACEROUTE_MAX_HOPS } from "./constants-B0OANIBL.mjs";
|
|
3
|
+
import { t as DetailedDataManager } from "./DetailedDataManager-BQQcxh64.mjs";
|
|
4
|
+
import { a as PerformanceMonitor } from "./modules-C184v-S9.mjs";
|
|
5
|
+
import { n as argEnum, t as argBool } from "./parse-args-BlRjqlkL.mjs";
|
|
6
|
+
import { a as isLoopbackHost, c as isSsrfTarget, i as isLocalSsrfBypassEnabled, l as resolveNetworkTarget, n as hasAuthorizedTargets, o as isNetworkAuthorizationExpired, r as isAuthorizedNetworkTarget, s as isPrivateHost, t as createNetworkAuthorizationPolicy } from "./ssrf-policy-ZaUfvhq7.mjs";
|
|
7
|
+
import { t as R } from "./ResponseBuilder-D3iFYx2N.mjs";
|
|
8
|
+
import "./definitions-CXEI7QC72.mjs";
|
|
9
|
+
import * as http$1 from "node:http";
|
|
10
|
+
import { promises } from "node:fs";
|
|
11
|
+
import koffi from "koffi";
|
|
12
|
+
import * as net from "node:net";
|
|
13
|
+
import * as tls from "node:tls";
|
|
14
|
+
import * as dns from "node:dns/promises";
|
|
15
|
+
import * as https from "node:https";
|
|
16
|
+
import * as http2 from "node:http2";
|
|
17
|
+
//#region src/server/domains/network/handlers.base.types.ts
|
|
18
|
+
/** Resource types excluded by default when no explicit filters are set. */
|
|
19
|
+
const EXCLUDED_RESOURCE_TYPES = new Set([
|
|
20
|
+
"Image",
|
|
21
|
+
"Font",
|
|
22
|
+
"Stylesheet",
|
|
23
|
+
"Media",
|
|
24
|
+
"Manifest",
|
|
25
|
+
"Ping"
|
|
26
|
+
]);
|
|
27
|
+
/** Priority order for smart sorting (lower = higher priority). */
|
|
28
|
+
const TYPE_SORT_PRIORITY = {
|
|
29
|
+
XHR: 0,
|
|
30
|
+
Fetch: 1,
|
|
31
|
+
Document: 2,
|
|
32
|
+
Script: 3,
|
|
33
|
+
WebSocket: 4,
|
|
34
|
+
EventSource: 5
|
|
35
|
+
};
|
|
36
|
+
const isObjectRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
37
|
+
const isNetworkRequestPayload = (value) => {
|
|
38
|
+
if (!isObjectRecord$1(value)) return false;
|
|
39
|
+
return typeof value.url === "string" && typeof value.method === "string";
|
|
40
|
+
};
|
|
41
|
+
const isNetworkResponsePayload = (value) => {
|
|
42
|
+
if (!isObjectRecord$1(value)) return false;
|
|
43
|
+
return typeof value.status === "number";
|
|
44
|
+
};
|
|
45
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
46
|
+
const asOptionalString = (value) => typeof value === "string" ? value : void 0;
|
|
47
|
+
const asOptionalBoolean = (value) => typeof value === "boolean" ? value : void 0;
|
|
48
|
+
const asOptionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
49
|
+
const asOptionalStringArray = (value) => {
|
|
50
|
+
if (!Array.isArray(value)) return;
|
|
51
|
+
return value.every((item) => typeof item === "string") ? value : void 0;
|
|
52
|
+
};
|
|
53
|
+
const isCpuProfileNodePayload = (value) => {
|
|
54
|
+
if (!isObjectRecord$1(value)) return false;
|
|
55
|
+
if (value.hitCount !== void 0 && typeof value.hitCount !== "number") return false;
|
|
56
|
+
if (value.callFrame !== void 0 && !isObjectRecord$1(value.callFrame)) return false;
|
|
57
|
+
if (isObjectRecord$1(value.callFrame)) {
|
|
58
|
+
if (value.callFrame.functionName !== void 0 && typeof value.callFrame.functionName !== "string") return false;
|
|
59
|
+
if (value.callFrame.url !== void 0 && typeof value.callFrame.url !== "string") return false;
|
|
60
|
+
if (value.callFrame.lineNumber !== void 0 && typeof value.callFrame.lineNumber !== "number") return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
const toCpuProfilePayload = (value) => {
|
|
65
|
+
if (!isObjectRecord$1(value)) return null;
|
|
66
|
+
if (!Array.isArray(value.nodes)) return null;
|
|
67
|
+
if (typeof value.startTime !== "number" || typeof value.endTime !== "number") return null;
|
|
68
|
+
if (!value.nodes.every((node) => isCpuProfileNodePayload(node))) return null;
|
|
69
|
+
return {
|
|
70
|
+
nodes: value.nodes,
|
|
71
|
+
samples: Array.isArray(value.samples) ? value.samples : void 0,
|
|
72
|
+
startTime: value.startTime,
|
|
73
|
+
endTime: value.endTime
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/server/domains/network/handlers/shared.ts
|
|
78
|
+
function getDetailedDataManager() {
|
|
79
|
+
return DetailedDataManager.getInstance();
|
|
80
|
+
}
|
|
81
|
+
function emitEvent(eventBus, event, payload) {
|
|
82
|
+
eventBus?.emit(event, payload);
|
|
83
|
+
}
|
|
84
|
+
function parseBooleanArg(value, defaultValue) {
|
|
85
|
+
if (typeof value === "boolean") return value;
|
|
86
|
+
if (typeof value === "number") {
|
|
87
|
+
if (value === 1) return true;
|
|
88
|
+
if (value === 0) return false;
|
|
89
|
+
return defaultValue;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
const normalized = value.trim().toLowerCase();
|
|
93
|
+
if ([
|
|
94
|
+
"true",
|
|
95
|
+
"1",
|
|
96
|
+
"yes",
|
|
97
|
+
"on"
|
|
98
|
+
].includes(normalized)) return true;
|
|
99
|
+
if ([
|
|
100
|
+
"false",
|
|
101
|
+
"0",
|
|
102
|
+
"no",
|
|
103
|
+
"off"
|
|
104
|
+
].includes(normalized)) return false;
|
|
105
|
+
}
|
|
106
|
+
return defaultValue;
|
|
107
|
+
}
|
|
108
|
+
function parseNumberArg(value, options) {
|
|
109
|
+
let parsed;
|
|
110
|
+
if (typeof value === "number" && Number.isFinite(value)) parsed = value;
|
|
111
|
+
else if (typeof value === "string") {
|
|
112
|
+
const trimmed = value.trim();
|
|
113
|
+
if (trimmed.length > 0) {
|
|
114
|
+
const n = Number(trimmed);
|
|
115
|
+
if (Number.isFinite(n)) parsed = n;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (parsed === void 0) parsed = options.defaultValue;
|
|
119
|
+
if (options.integer) parsed = Math.trunc(parsed);
|
|
120
|
+
if (typeof options.min === "number") parsed = Math.max(options.min, parsed);
|
|
121
|
+
if (typeof options.max === "number") parsed = Math.min(options.max, parsed);
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/server/domains/network/handlers/core-handlers.ts
|
|
126
|
+
/**
|
|
127
|
+
* Core network handlers — enable/disable/status/requests/response/stats.
|
|
128
|
+
*
|
|
129
|
+
* Extracted from NetworkHandlersCore (handlers.base.core.ts).
|
|
130
|
+
*/
|
|
131
|
+
var CoreHandlers = class {
|
|
132
|
+
detailedDataManager = getDetailedDataManager();
|
|
133
|
+
constructor(deps) {
|
|
134
|
+
this.deps = deps;
|
|
135
|
+
}
|
|
136
|
+
async handleNetworkMonitor(args) {
|
|
137
|
+
const action = String(args["action"] ?? "");
|
|
138
|
+
switch (action) {
|
|
139
|
+
case "enable": return this.handleNetworkEnable(args);
|
|
140
|
+
case "disable": return this.handleNetworkDisable(args);
|
|
141
|
+
case "status": return this.handleNetworkGetStatus(args);
|
|
142
|
+
default: return R.fail(`Invalid generic action parameter: ${action}. Expected enable, disable, status.`).json();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async handleNetworkEnable(args) {
|
|
146
|
+
try {
|
|
147
|
+
const enableExceptions = parseBooleanArg(args.enableExceptions, true);
|
|
148
|
+
await this.deps.consoleMonitor.enable({
|
|
149
|
+
enableNetwork: true,
|
|
150
|
+
enableExceptions
|
|
151
|
+
});
|
|
152
|
+
const status = this.deps.consoleMonitor.getNetworkStatus();
|
|
153
|
+
return R.ok().merge({
|
|
154
|
+
message: " Network monitoring enabled successfully",
|
|
155
|
+
enabled: status.enabled,
|
|
156
|
+
cdpSessionActive: status.cdpSessionActive,
|
|
157
|
+
listenerCount: status.listenerCount,
|
|
158
|
+
usage: {
|
|
159
|
+
step1: "Network monitoring is now active",
|
|
160
|
+
step2: "Navigate to a page using page_navigate tool",
|
|
161
|
+
step3: "Use network_get_requests to retrieve captured requests",
|
|
162
|
+
step4: "Use network_get_response_body to get response content"
|
|
163
|
+
},
|
|
164
|
+
important: "Network monitoring must be enabled BEFORE navigating to capture requests"
|
|
165
|
+
}).json();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return R.fail(error).json();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async handleNetworkDisable(_args) {
|
|
171
|
+
try {
|
|
172
|
+
await this.deps.consoleMonitor.disable();
|
|
173
|
+
return R.ok().set("message", "Network monitoring disabled").json();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return R.fail(error).json();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async handleNetworkGetStatus(_args) {
|
|
179
|
+
try {
|
|
180
|
+
const status = this.deps.consoleMonitor.getNetworkStatus();
|
|
181
|
+
if (!status.enabled) return R.fail(" Network monitoring is NOT enabled").merge({
|
|
182
|
+
enabled: false,
|
|
183
|
+
nextSteps: {
|
|
184
|
+
step1: "Call network_enable tool to start monitoring",
|
|
185
|
+
step2: "Then navigate to a page using page_navigate",
|
|
186
|
+
step3: "Finally use network_get_requests to see captured requests"
|
|
187
|
+
},
|
|
188
|
+
example: "network_enable -> page_navigate -> network_get_requests"
|
|
189
|
+
}).json();
|
|
190
|
+
return R.ok().merge({
|
|
191
|
+
enabled: true,
|
|
192
|
+
message: ` Network monitoring is active. Captured ${status.requestCount} requests and ${status.responseCount} responses.`,
|
|
193
|
+
requestCount: status.requestCount,
|
|
194
|
+
responseCount: status.responseCount,
|
|
195
|
+
listenerCount: status.listenerCount,
|
|
196
|
+
cdpSessionActive: status.cdpSessionActive,
|
|
197
|
+
nextSteps: status.requestCount === 0 ? {
|
|
198
|
+
hint: "No requests captured yet",
|
|
199
|
+
action: "Navigate to a page using page_navigate to capture network traffic"
|
|
200
|
+
} : {
|
|
201
|
+
hint: `${status.requestCount} requests captured`,
|
|
202
|
+
action: "Use network_get_requests to retrieve them"
|
|
203
|
+
}
|
|
204
|
+
}).json();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return R.fail(error).json();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async handleNetworkGetRequests(args) {
|
|
210
|
+
try {
|
|
211
|
+
const autoEnable = parseBooleanArg(args.autoEnable, true);
|
|
212
|
+
const enableExceptions = parseBooleanArg(args.enableExceptions, true);
|
|
213
|
+
const networkState = await this.ensureNetworkEnabled({
|
|
214
|
+
autoEnable,
|
|
215
|
+
enableExceptions
|
|
216
|
+
});
|
|
217
|
+
if (!networkState.enabled) return this.buildNotEnabledResponse(autoEnable, networkState.error);
|
|
218
|
+
const url = asOptionalString(args.url);
|
|
219
|
+
const urlRegex = asOptionalString(args.urlRegex);
|
|
220
|
+
const method = asOptionalString(args.method);
|
|
221
|
+
const sinceTimestamp = isFiniteNumber(args.sinceTimestamp) ? args.sinceTimestamp : void 0;
|
|
222
|
+
const sinceRequestId = asOptionalString(args.sinceRequestId);
|
|
223
|
+
const tail = isFiniteNumber(args.tail) && args.tail > 0 ? Math.floor(args.tail) : void 0;
|
|
224
|
+
const limit = parseNumberArg(args.limit, {
|
|
225
|
+
defaultValue: 100,
|
|
226
|
+
min: 1,
|
|
227
|
+
max: 1e3,
|
|
228
|
+
integer: true
|
|
229
|
+
});
|
|
230
|
+
const offset = parseNumberArg(args.offset, {
|
|
231
|
+
defaultValue: 0,
|
|
232
|
+
min: 0,
|
|
233
|
+
integer: true
|
|
234
|
+
});
|
|
235
|
+
const requests = this.deps.consoleMonitor.getNetworkRequests().filter((req) => isNetworkRequestPayload(req)).map((r) => r);
|
|
236
|
+
if (requests.length === 0) return this.buildEmptyRequestsResponse(networkState.autoEnabled);
|
|
237
|
+
const result = this.applyRequestFilters(requests, {
|
|
238
|
+
url,
|
|
239
|
+
urlRegex,
|
|
240
|
+
method,
|
|
241
|
+
sinceTimestamp,
|
|
242
|
+
sinceRequestId,
|
|
243
|
+
tail,
|
|
244
|
+
limit,
|
|
245
|
+
offset
|
|
246
|
+
});
|
|
247
|
+
const processedResult = this.detailedDataManager.smartHandle(result.finalPayload, 25600);
|
|
248
|
+
return R.ok().merge(processedResult).json();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return R.fail(error).json();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async handleNetworkGetResponseBody(args) {
|
|
254
|
+
try {
|
|
255
|
+
const requestId = asOptionalString(args.requestId) || "";
|
|
256
|
+
const maxSize = parseNumberArg(args.maxSize, {
|
|
257
|
+
defaultValue: 1e5,
|
|
258
|
+
min: 1024,
|
|
259
|
+
max: 20 * 1024 * 1024,
|
|
260
|
+
integer: true
|
|
261
|
+
});
|
|
262
|
+
const returnSummary = parseBooleanArg(args.returnSummary, false);
|
|
263
|
+
const retries = parseNumberArg(args.retries, {
|
|
264
|
+
defaultValue: 3,
|
|
265
|
+
min: 0,
|
|
266
|
+
max: 10,
|
|
267
|
+
integer: true
|
|
268
|
+
});
|
|
269
|
+
const retryIntervalMs = parseNumberArg(args.retryIntervalMs, {
|
|
270
|
+
defaultValue: 500,
|
|
271
|
+
min: 50,
|
|
272
|
+
max: 5e3,
|
|
273
|
+
integer: true
|
|
274
|
+
});
|
|
275
|
+
const autoEnable = parseBooleanArg(args.autoEnable, false);
|
|
276
|
+
const enableExceptions = parseBooleanArg(args.enableExceptions, true);
|
|
277
|
+
if (!requestId) return R.fail("requestId parameter is required").set("hint", "Get requestId from network_get_requests tool").json();
|
|
278
|
+
const networkState = await this.ensureNetworkEnabled({
|
|
279
|
+
autoEnable,
|
|
280
|
+
enableExceptions
|
|
281
|
+
});
|
|
282
|
+
if (!networkState.enabled) return R.fail("Network monitoring is not enabled").merge({
|
|
283
|
+
hint: autoEnable ? "Auto-enable failed. Check active page and call network_enable manually." : "Use network_enable tool first, or set autoEnable=true",
|
|
284
|
+
detail: networkState.error
|
|
285
|
+
}).json();
|
|
286
|
+
let body = null;
|
|
287
|
+
let attemptsMade = 0;
|
|
288
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
289
|
+
attemptsMade = attempt + 1;
|
|
290
|
+
body = await this.deps.consoleMonitor.getResponseBody(requestId);
|
|
291
|
+
if (body) break;
|
|
292
|
+
if (attempt < retries) await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
|
|
293
|
+
}
|
|
294
|
+
if (!body) return R.fail(`No response body found for requestId: ${requestId}`).merge({
|
|
295
|
+
hint: "The request may not have completed yet, or the requestId is invalid",
|
|
296
|
+
attempts: attemptsMade,
|
|
297
|
+
waitedMs: retries * retryIntervalMs,
|
|
298
|
+
retryConfig: {
|
|
299
|
+
retries,
|
|
300
|
+
retryIntervalMs
|
|
301
|
+
}
|
|
302
|
+
}).json();
|
|
303
|
+
return this.buildResponseBodyResult(requestId, body, attemptsMade, maxSize, returnSummary);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return R.fail(error).json();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async handleNetworkGetStats(_args) {
|
|
309
|
+
try {
|
|
310
|
+
if (!this.deps.consoleMonitor.isNetworkEnabled()) return R.fail("Network monitoring is not enabled").set("hint", "Use network_enable tool first").json();
|
|
311
|
+
const requests = this.deps.consoleMonitor.getNetworkRequests().filter((req) => isNetworkRequestPayload(req));
|
|
312
|
+
const responses = this.deps.consoleMonitor.getNetworkResponses().filter(isNetworkResponsePayload);
|
|
313
|
+
const byMethod = {};
|
|
314
|
+
requests.forEach((req) => {
|
|
315
|
+
byMethod[req.method] = (byMethod[req.method] || 0) + 1;
|
|
316
|
+
});
|
|
317
|
+
const byStatus = {};
|
|
318
|
+
responses.forEach((res) => {
|
|
319
|
+
byStatus[res.status] = (byStatus[res.status] || 0) + 1;
|
|
320
|
+
});
|
|
321
|
+
const byType = {};
|
|
322
|
+
requests.forEach((req) => {
|
|
323
|
+
const type = req.type || "unknown";
|
|
324
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
325
|
+
});
|
|
326
|
+
const timestamps = requests.map((r) => r.timestamp).filter((t) => isFiniteNumber(t));
|
|
327
|
+
const timeStats = timestamps.length > 0 ? {
|
|
328
|
+
earliest: Math.min(...timestamps),
|
|
329
|
+
latest: Math.max(...timestamps),
|
|
330
|
+
duration: Math.max(...timestamps) - Math.min(...timestamps)
|
|
331
|
+
} : null;
|
|
332
|
+
return R.ok().set("stats", {
|
|
333
|
+
totalRequests: requests.length,
|
|
334
|
+
totalResponses: responses.length,
|
|
335
|
+
byMethod,
|
|
336
|
+
byStatus,
|
|
337
|
+
byType,
|
|
338
|
+
timeStats,
|
|
339
|
+
monitoringEnabled: true
|
|
340
|
+
}).json();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return R.fail(error).json();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async ensureNetworkEnabled(options) {
|
|
346
|
+
if (this.deps.consoleMonitor.isNetworkEnabled()) return {
|
|
347
|
+
enabled: true,
|
|
348
|
+
autoEnabled: false
|
|
349
|
+
};
|
|
350
|
+
if (!options.autoEnable) return {
|
|
351
|
+
enabled: false,
|
|
352
|
+
autoEnabled: false
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
await this.deps.consoleMonitor.enable({
|
|
356
|
+
enableNetwork: true,
|
|
357
|
+
enableExceptions: options.enableExceptions
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
enabled: this.deps.consoleMonitor.isNetworkEnabled(),
|
|
361
|
+
autoEnabled: true
|
|
362
|
+
};
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return {
|
|
365
|
+
enabled: false,
|
|
366
|
+
autoEnabled: false,
|
|
367
|
+
error: error instanceof Error ? error.message : String(error)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
buildNotEnabledResponse(autoEnable, error) {
|
|
372
|
+
if (autoEnable && error) return R.fail("Failed to auto-enable network monitoring").merge({
|
|
373
|
+
detail: error,
|
|
374
|
+
solution: {
|
|
375
|
+
step1: "Ensure browser page is active and reachable",
|
|
376
|
+
step2: "Call network_enable manually",
|
|
377
|
+
step3: "Navigate to target page: page_navigate(url)",
|
|
378
|
+
step4: "Get requests: network_get_requests"
|
|
379
|
+
}
|
|
380
|
+
}).json();
|
|
381
|
+
return R.fail(" Network monitoring is not enabled").merge({
|
|
382
|
+
requests: [],
|
|
383
|
+
total: 0,
|
|
384
|
+
solution: {
|
|
385
|
+
step1: "Enable network monitoring: network_enable",
|
|
386
|
+
step2: "Navigate to target page: page_navigate(url)",
|
|
387
|
+
step3: "Get requests: network_get_requests"
|
|
388
|
+
},
|
|
389
|
+
tip: "Set autoEnable=true to auto-enable monitoring in this call"
|
|
390
|
+
}).json();
|
|
391
|
+
}
|
|
392
|
+
buildEmptyRequestsResponse(autoEnabled) {
|
|
393
|
+
return R.ok().merge({
|
|
394
|
+
message: "No network requests captured yet",
|
|
395
|
+
requests: [],
|
|
396
|
+
total: 0,
|
|
397
|
+
hint: "Network monitoring is enabled, but no requests have been captured",
|
|
398
|
+
possibleReasons: [
|
|
399
|
+
"1. You haven't navigated to any page yet (use page_navigate)",
|
|
400
|
+
"2. The page has already loaded before network monitoring was enabled",
|
|
401
|
+
"3. The page doesn't make any network requests",
|
|
402
|
+
"4. The page uses frontend-wrapped fetch/XHR not captured by CDP"
|
|
403
|
+
],
|
|
404
|
+
recommended_actions: [
|
|
405
|
+
"console_inject_fetch_interceptor() — capture frontend-wrapped fetch calls (SPAs, React, Vue)",
|
|
406
|
+
"console_inject_xhr_interceptor() — capture XMLHttpRequest calls",
|
|
407
|
+
"page_navigate(url, enableNetworkMonitoring=true) — re-navigate with monitoring enabled"
|
|
408
|
+
],
|
|
409
|
+
nextAction: "Call console_inject_fetch_interceptor(), then re-navigate or trigger the target action",
|
|
410
|
+
monitoring: { autoEnabled }
|
|
411
|
+
}).json();
|
|
412
|
+
}
|
|
413
|
+
applyRequestFilters(requests, filters) {
|
|
414
|
+
const { url, urlRegex, method, sinceTimestamp, sinceRequestId, tail, limit, offset } = filters;
|
|
415
|
+
const originalCount = requests.length;
|
|
416
|
+
const allUrls = requests.map((r) => r.url);
|
|
417
|
+
const hasAnyFilter = !!(url || urlRegex || method && method.toUpperCase() !== "ALL" || sinceTimestamp || sinceRequestId || tail);
|
|
418
|
+
let excludedStaticCount = 0;
|
|
419
|
+
if (!hasAnyFilter) {
|
|
420
|
+
const beforeTypeFilter = requests.length;
|
|
421
|
+
requests = requests.filter((r) => !r.type || !EXCLUDED_RESOURCE_TYPES.has(r.type));
|
|
422
|
+
excludedStaticCount = beforeTypeFilter - requests.length;
|
|
423
|
+
}
|
|
424
|
+
if (sinceRequestId) {
|
|
425
|
+
const idx = requests.findIndex((r) => r.requestId === sinceRequestId);
|
|
426
|
+
if (idx >= 0) requests = requests.slice(idx + 1);
|
|
427
|
+
}
|
|
428
|
+
if (sinceTimestamp !== void 0) requests = requests.filter((r) => (r.timestamp ?? 0) > sinceTimestamp);
|
|
429
|
+
if (urlRegex) {
|
|
430
|
+
if (urlRegex.length > 500) throw new Error("urlRegex too long (max 500 characters)");
|
|
431
|
+
const re = new RegExp(urlRegex, "i");
|
|
432
|
+
if (requests.length > 0) {
|
|
433
|
+
const start = performance.now();
|
|
434
|
+
re.test(requests[0].url);
|
|
435
|
+
const elapsed = performance.now() - start;
|
|
436
|
+
if (elapsed > 100) throw new Error(`urlRegex pattern is too expensive (${elapsed.toFixed(0)}ms on first URL). Use a simpler pattern.`);
|
|
437
|
+
}
|
|
438
|
+
requests = requests.filter((req) => re.test(req.url));
|
|
439
|
+
} else if (url) {
|
|
440
|
+
const urlLower = url.toLowerCase();
|
|
441
|
+
requests = requests.filter((req) => req.url.toLowerCase().includes(urlLower));
|
|
442
|
+
}
|
|
443
|
+
if (method && method.toUpperCase() !== "ALL") requests = requests.filter((req) => req.method.toUpperCase() === method.toUpperCase());
|
|
444
|
+
if (tail !== void 0 && requests.length > tail) requests = requests.slice(-tail);
|
|
445
|
+
requests.sort((a, b) => (TYPE_SORT_PRIORITY[a.type ?? ""] ?? 6) - (TYPE_SORT_PRIORITY[b.type ?? ""] ?? 6));
|
|
446
|
+
const beforeLimit = requests.length;
|
|
447
|
+
requests = requests.slice(offset, offset + limit);
|
|
448
|
+
const hasMore = offset + requests.length < beforeLimit;
|
|
449
|
+
const filterMiss = beforeLimit === 0 && originalCount > 0 && !!(url || method && method.toUpperCase() !== "ALL");
|
|
450
|
+
const urlSamples = filterMiss ? allUrls.slice(0, 10).map((u) => u.substring(0, 120)) : void 0;
|
|
451
|
+
return { finalPayload: {
|
|
452
|
+
message: ` Retrieved ${requests.length} network request(s)`,
|
|
453
|
+
requests,
|
|
454
|
+
total: requests.length,
|
|
455
|
+
page: {
|
|
456
|
+
offset,
|
|
457
|
+
limit,
|
|
458
|
+
returned: requests.length,
|
|
459
|
+
totalAfterFilter: beforeLimit,
|
|
460
|
+
hasMore,
|
|
461
|
+
nextOffset: hasMore ? offset + requests.length : null
|
|
462
|
+
},
|
|
463
|
+
stats: {
|
|
464
|
+
totalCaptured: originalCount,
|
|
465
|
+
afterFilter: beforeLimit,
|
|
466
|
+
returned: requests.length,
|
|
467
|
+
truncated: beforeLimit > offset + limit
|
|
468
|
+
},
|
|
469
|
+
filtered: hasAnyFilter,
|
|
470
|
+
filters: {
|
|
471
|
+
url,
|
|
472
|
+
urlRegex,
|
|
473
|
+
method,
|
|
474
|
+
sinceTimestamp,
|
|
475
|
+
sinceRequestId,
|
|
476
|
+
tail,
|
|
477
|
+
limit,
|
|
478
|
+
offset
|
|
479
|
+
},
|
|
480
|
+
monitoring: {},
|
|
481
|
+
...filterMiss && {
|
|
482
|
+
filterMiss: true,
|
|
483
|
+
hint: `URL filter "${url}" matched 0 of ${originalCount} captured requests. Check urlSamples to verify the correct filter substring.`,
|
|
484
|
+
urlSamples
|
|
485
|
+
},
|
|
486
|
+
tip: requests.length > 0 ? "Use network_get_response_body(requestId) to get response content" : void 0,
|
|
487
|
+
...excludedStaticCount > 0 && {
|
|
488
|
+
staticResourcesExcluded: excludedStaticCount,
|
|
489
|
+
staticFilterNote: `${excludedStaticCount} static resources (Image/Font/Stylesheet/Media) excluded by default. Set any filter to include all types.`
|
|
490
|
+
},
|
|
491
|
+
...originalCount > 100 && !hasAnyFilter && { optimizationHint: `${originalCount} requests captured. Use url/method filters to reduce payload size.` }
|
|
492
|
+
} };
|
|
493
|
+
}
|
|
494
|
+
buildResponseBodyResult(requestId, body, attemptsMade, maxSize, returnSummary) {
|
|
495
|
+
const originalSize = body.body.length;
|
|
496
|
+
const isTooLarge = originalSize > maxSize;
|
|
497
|
+
if (returnSummary || isTooLarge) {
|
|
498
|
+
const preview = body.body.substring(0, 500);
|
|
499
|
+
return R.ok().merge({
|
|
500
|
+
requestId,
|
|
501
|
+
attempts: attemptsMade,
|
|
502
|
+
summary: {
|
|
503
|
+
size: originalSize,
|
|
504
|
+
sizeKB: (originalSize / 1024).toFixed(2),
|
|
505
|
+
base64Encoded: body.base64Encoded,
|
|
506
|
+
preview: preview + (originalSize > 500 ? "..." : ""),
|
|
507
|
+
truncated: isTooLarge,
|
|
508
|
+
reason: isTooLarge ? `Response too large (${(originalSize / 1024).toFixed(2)} KB > ${(maxSize / 1024).toFixed(2)} KB)` : "Summary mode enabled"
|
|
509
|
+
},
|
|
510
|
+
tip: isTooLarge ? "Use collect_code tool to collect and compress this script, or increase maxSize parameter" : "Set returnSummary=false to get full body"
|
|
511
|
+
}).json();
|
|
512
|
+
}
|
|
513
|
+
return R.ok().merge({
|
|
514
|
+
requestId,
|
|
515
|
+
attempts: attemptsMade,
|
|
516
|
+
body: body.body,
|
|
517
|
+
base64Encoded: body.base64Encoded,
|
|
518
|
+
size: originalSize,
|
|
519
|
+
sizeKB: (originalSize / 1024).toFixed(2)
|
|
520
|
+
}).json();
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/server/domains/network/handlers/performance-handlers.ts
|
|
525
|
+
var PerformanceHandlers = class {
|
|
526
|
+
constructor(deps) {
|
|
527
|
+
this.deps = deps;
|
|
528
|
+
}
|
|
529
|
+
async handlePerformanceGetMetrics(args) {
|
|
530
|
+
try {
|
|
531
|
+
const includeTimeline = args.includeTimeline === true;
|
|
532
|
+
const monitor = this.deps.getPerformanceMonitor();
|
|
533
|
+
const metrics = await monitor.getPerformanceMetrics();
|
|
534
|
+
const builder = R.ok().set("metrics", metrics);
|
|
535
|
+
if (includeTimeline) builder.set("timeline", await monitor.getPerformanceTimeline());
|
|
536
|
+
return builder.json();
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return R.fail(error).json();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async handlePerformanceCoverage(args) {
|
|
542
|
+
return argEnum(args, "action", new Set(["start", "stop"])) === "stop" ? this.handlePerformanceStopCoverage(args) : this.handlePerformanceStartCoverage(args);
|
|
543
|
+
}
|
|
544
|
+
async handlePerformanceStartCoverage(_args) {
|
|
545
|
+
try {
|
|
546
|
+
await this.deps.getPerformanceMonitor().startCoverage();
|
|
547
|
+
return R.ok().set("message", "Code coverage collection started").json();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return R.fail(error).json();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async handlePerformanceStopCoverage(_args) {
|
|
553
|
+
try {
|
|
554
|
+
const coverage = await this.deps.getPerformanceMonitor().stopCoverage();
|
|
555
|
+
const avgCoverage = coverage.length > 0 ? coverage.reduce((sum, info) => sum + info.coveragePercentage, 0) / coverage.length : 0;
|
|
556
|
+
return R.ok().merge({
|
|
557
|
+
coverage,
|
|
558
|
+
totalScripts: coverage.length,
|
|
559
|
+
avgCoverage
|
|
560
|
+
}).json();
|
|
561
|
+
} catch (error) {
|
|
562
|
+
return R.fail(error).json();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async handlePerformanceTakeHeapSnapshot(_args) {
|
|
566
|
+
try {
|
|
567
|
+
const snapshotSize = await this.deps.getPerformanceMonitor().takeHeapSnapshot();
|
|
568
|
+
return R.ok().merge({
|
|
569
|
+
snapshotSize,
|
|
570
|
+
message: "Heap snapshot taken (data too large to return, saved internally)"
|
|
571
|
+
}).json();
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return R.fail(error).json();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async handlePerformanceTraceStart(args) {
|
|
577
|
+
try {
|
|
578
|
+
const monitor = this.deps.getPerformanceMonitor();
|
|
579
|
+
const categories = asOptionalStringArray(args.categories);
|
|
580
|
+
const screenshots = asOptionalBoolean(args.screenshots);
|
|
581
|
+
await monitor.startTracing({
|
|
582
|
+
categories,
|
|
583
|
+
screenshots
|
|
584
|
+
});
|
|
585
|
+
return R.ok().set("message", "Performance tracing started. Call performance_trace_stop to save the trace.").json();
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return R.fail(error).json();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async handlePerformanceTraceStop(args) {
|
|
591
|
+
try {
|
|
592
|
+
const monitor = this.deps.getPerformanceMonitor();
|
|
593
|
+
const artifactPath = asOptionalString(args.artifactPath);
|
|
594
|
+
const result = await monitor.stopTracing({ artifactPath });
|
|
595
|
+
return R.ok().merge({
|
|
596
|
+
artifactPath: result.artifactPath,
|
|
597
|
+
eventCount: result.eventCount,
|
|
598
|
+
sizeBytes: result.sizeBytes,
|
|
599
|
+
sizeKB: (result.sizeBytes / 1024).toFixed(1),
|
|
600
|
+
hint: "Open the trace file in Chrome DevTools -> Performance tab -> Load profile"
|
|
601
|
+
}).json();
|
|
602
|
+
} catch (error) {
|
|
603
|
+
return R.fail(error).json();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async handleProfilerCpuStart(_args) {
|
|
607
|
+
try {
|
|
608
|
+
await this.deps.getPerformanceMonitor().startCPUProfiling();
|
|
609
|
+
return R.ok().set("message", "CPU profiling started. Call profiler_cpu_stop to save the profile.").json();
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return R.fail(error).json();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async handleProfilerCpuStop(args) {
|
|
615
|
+
try {
|
|
616
|
+
const profileRaw = await this.deps.getPerformanceMonitor().stopCPUProfiling();
|
|
617
|
+
const profile = toCpuProfilePayload(profileRaw) || profileRaw;
|
|
618
|
+
const { writeFile } = await import("node:fs/promises");
|
|
619
|
+
const { resolveArtifactPath } = await import("./artifacts-Bk2-_uPq.mjs").then((n) => n.t);
|
|
620
|
+
const artifactPath = asOptionalString(args.artifactPath);
|
|
621
|
+
const profileJson = JSON.stringify(profile, null, 2);
|
|
622
|
+
let savedPath;
|
|
623
|
+
if (artifactPath) {
|
|
624
|
+
await writeFile(artifactPath, profileJson, "utf-8");
|
|
625
|
+
savedPath = artifactPath;
|
|
626
|
+
} else {
|
|
627
|
+
const { absolutePath, displayPath } = await resolveArtifactPath({
|
|
628
|
+
category: "profiles",
|
|
629
|
+
toolName: "cpu-profile",
|
|
630
|
+
ext: "cpuprofile"
|
|
631
|
+
});
|
|
632
|
+
await writeFile(absolutePath, profileJson, "utf-8");
|
|
633
|
+
savedPath = displayPath;
|
|
634
|
+
}
|
|
635
|
+
const hotFunctions = profile.nodes.filter((n) => (n.hitCount || 0) > 0).toSorted((a, b) => (b.hitCount || 0) - (a.hitCount || 0)).slice(0, 20).map((n) => ({
|
|
636
|
+
functionName: n.callFrame?.functionName || "(anonymous)",
|
|
637
|
+
url: n.callFrame?.url,
|
|
638
|
+
line: n.callFrame?.lineNumber,
|
|
639
|
+
hitCount: n.hitCount
|
|
640
|
+
}));
|
|
641
|
+
return R.ok().merge({
|
|
642
|
+
artifactPath: savedPath,
|
|
643
|
+
totalNodes: profile.nodes.length,
|
|
644
|
+
totalSamples: profile.samples?.length || 0,
|
|
645
|
+
durationMs: profile.endTime - profile.startTime,
|
|
646
|
+
hotFunctions,
|
|
647
|
+
hint: "Open the .cpuprofile file in Chrome DevTools -> Performance tab"
|
|
648
|
+
}).json();
|
|
649
|
+
} catch (error) {
|
|
650
|
+
return R.fail(error).json();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async handleProfilerHeapSamplingStart(args) {
|
|
654
|
+
try {
|
|
655
|
+
const monitor = this.deps.getPerformanceMonitor();
|
|
656
|
+
const samplingInterval = asOptionalNumber(args.samplingInterval);
|
|
657
|
+
await monitor.startHeapSampling({ samplingInterval });
|
|
658
|
+
return R.ok().set("message", "Heap sampling started. Call profiler_heap_sampling_stop to save the report.").json();
|
|
659
|
+
} catch (error) {
|
|
660
|
+
return R.fail(error).json();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async handleProfilerHeapSamplingStop(args) {
|
|
664
|
+
try {
|
|
665
|
+
const monitor = this.deps.getPerformanceMonitor();
|
|
666
|
+
const artifactPath = asOptionalString(args.artifactPath);
|
|
667
|
+
const topN = asOptionalNumber(args.topN);
|
|
668
|
+
const result = await monitor.stopHeapSampling({
|
|
669
|
+
artifactPath,
|
|
670
|
+
topN
|
|
671
|
+
});
|
|
672
|
+
return R.ok().merge({
|
|
673
|
+
artifactPath: result.artifactPath,
|
|
674
|
+
sampleCount: result.sampleCount,
|
|
675
|
+
topAllocations: result.topAllocations
|
|
676
|
+
}).json();
|
|
677
|
+
} catch (error) {
|
|
678
|
+
return R.fail(error).json();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/server/domains/network/handlers/console-handlers.ts
|
|
684
|
+
/**
|
|
685
|
+
* Console exception, interceptor, tracer, and monitoring handlers.
|
|
686
|
+
*
|
|
687
|
+
* Extracted from AdvancedHandlersBase (handlers.base.ts).
|
|
688
|
+
*/
|
|
689
|
+
var ConsoleHandlers = class {
|
|
690
|
+
constructor(deps) {
|
|
691
|
+
this.deps = deps;
|
|
692
|
+
}
|
|
693
|
+
async handleConsoleGetExceptions(args) {
|
|
694
|
+
try {
|
|
695
|
+
const url = asOptionalString(args.url);
|
|
696
|
+
const limit = parseNumberArg(args.limit, {
|
|
697
|
+
defaultValue: 50,
|
|
698
|
+
min: 1,
|
|
699
|
+
max: 1e3,
|
|
700
|
+
integer: true
|
|
701
|
+
});
|
|
702
|
+
let exceptions = this.deps.consoleMonitor.getExceptions();
|
|
703
|
+
if (url) exceptions = exceptions.filter((ex) => ex.url?.includes(url));
|
|
704
|
+
exceptions = exceptions.slice(0, limit);
|
|
705
|
+
return R.ok().merge({
|
|
706
|
+
exceptions,
|
|
707
|
+
total: exceptions.length
|
|
708
|
+
}).json();
|
|
709
|
+
} catch (error) {
|
|
710
|
+
return R.fail(error).json();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async handleConsoleInjectScriptMonitor(args) {
|
|
714
|
+
try {
|
|
715
|
+
const persistent = argBool(args, "persistent", false);
|
|
716
|
+
await this.deps.consoleMonitor.enableDynamicScriptMonitoring({ persistent });
|
|
717
|
+
return R.ok().set("message", persistent ? "Dynamic script monitoring enabled (persistent — survives navigations)" : "Dynamic script monitoring enabled").json();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
return R.fail(error).json();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async handleConsoleInjectXhrInterceptor(args) {
|
|
723
|
+
try {
|
|
724
|
+
const persistent = argBool(args, "persistent", false);
|
|
725
|
+
await this.deps.consoleMonitor.injectXHRInterceptor({ persistent });
|
|
726
|
+
return R.ok().set("message", persistent ? "XHR interceptor injected (persistent)" : "XHR interceptor injected").json();
|
|
727
|
+
} catch (error) {
|
|
728
|
+
return R.fail(error).json();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async handleConsoleInjectFetchInterceptor(args) {
|
|
732
|
+
try {
|
|
733
|
+
const persistent = argBool(args, "persistent", false);
|
|
734
|
+
await this.deps.consoleMonitor.injectFetchInterceptor({ persistent });
|
|
735
|
+
return R.ok().set("message", persistent ? "Fetch interceptor injected (persistent)" : "Fetch interceptor injected").json();
|
|
736
|
+
} catch (error) {
|
|
737
|
+
return R.fail(error).json();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async handleConsoleClearInjectedBuffers(_args) {
|
|
741
|
+
try {
|
|
742
|
+
const result = await this.deps.consoleMonitor.clearInjectedBuffers();
|
|
743
|
+
return R.ok().merge({
|
|
744
|
+
message: "Injected buffers cleared",
|
|
745
|
+
...result
|
|
746
|
+
}).json();
|
|
747
|
+
} catch (error) {
|
|
748
|
+
return R.fail(error).json();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async handleConsoleResetInjectedInterceptors(_args) {
|
|
752
|
+
try {
|
|
753
|
+
const result = await this.deps.consoleMonitor.resetInjectedInterceptors();
|
|
754
|
+
return R.ok().merge({
|
|
755
|
+
message: "Injected interceptors/monitors reset",
|
|
756
|
+
...result
|
|
757
|
+
}).json();
|
|
758
|
+
} catch (error) {
|
|
759
|
+
return R.fail(error).json();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async handleConsoleInjectFunctionTracer(args) {
|
|
763
|
+
try {
|
|
764
|
+
const functionName = asOptionalString(args.functionName) || "";
|
|
765
|
+
if (!functionName) return R.fail("functionName is required").json();
|
|
766
|
+
const persistent = argBool(args, "persistent", false);
|
|
767
|
+
await this.deps.consoleMonitor.injectFunctionTracer(functionName, { persistent });
|
|
768
|
+
return R.ok().set("message", persistent ? `Function tracer injected for: ${functionName} (persistent — survives navigations)` : `Function tracer injected for: ${functionName}`).json();
|
|
769
|
+
} catch (error) {
|
|
770
|
+
return R.fail(error).json();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/server/domains/network/auth-extractor.ts
|
|
776
|
+
const AUTH_HEADER_KEYS = [
|
|
777
|
+
"authorization",
|
|
778
|
+
"cookie",
|
|
779
|
+
"x-token",
|
|
780
|
+
"x-auth-token",
|
|
781
|
+
"x-access-token",
|
|
782
|
+
"x-api-key",
|
|
783
|
+
"x-signature",
|
|
784
|
+
"x-sign",
|
|
785
|
+
"x-csrf-token"
|
|
786
|
+
];
|
|
787
|
+
const TOKEN_BODY_KEYS = /^(token|access_token|refresh_token|sign|signature|auth|jwt|api_key|apikey|key|secret)$/i;
|
|
788
|
+
const JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
789
|
+
const BEARER_RE = /^Bearer\s+\S+/i;
|
|
790
|
+
function maskSecret(raw) {
|
|
791
|
+
const trimmed = raw.trim();
|
|
792
|
+
if (trimmed.length <= 12) return "***";
|
|
793
|
+
return `${trimmed.slice(0, 6)}***${trimmed.slice(-4)}`;
|
|
794
|
+
}
|
|
795
|
+
function scoreValue(value) {
|
|
796
|
+
const v = value.trim();
|
|
797
|
+
if (BEARER_RE.test(v)) return .95;
|
|
798
|
+
if (JWT_RE.test(v)) return .9;
|
|
799
|
+
if (v.length > 20 && /^[A-Za-z0-9+/=_-]+$/.test(v)) return .7;
|
|
800
|
+
if (v.length > 10) return .5;
|
|
801
|
+
return .3;
|
|
802
|
+
}
|
|
803
|
+
function extractAuthFromRequests(requests) {
|
|
804
|
+
const findings = [];
|
|
805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const req of requests) {
|
|
807
|
+
const headers = req.headers ?? {};
|
|
808
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
809
|
+
const lk = k.toLowerCase();
|
|
810
|
+
if (!AUTH_HEADER_KEYS.includes(lk)) continue;
|
|
811
|
+
if (!v || v.length < 4) continue;
|
|
812
|
+
if (lk === "cookie") {
|
|
813
|
+
for (const part of v.split(";")) {
|
|
814
|
+
const eqIdx = part.indexOf("=");
|
|
815
|
+
if (eqIdx === -1) continue;
|
|
816
|
+
const name = part.slice(0, eqIdx).trim();
|
|
817
|
+
const val = part.slice(eqIdx + 1).trim();
|
|
818
|
+
if (!val || val.length < 8) continue;
|
|
819
|
+
const dedupeKey = `cookie:${name}:${val.slice(0, 8)}`;
|
|
820
|
+
if (seen.has(dedupeKey)) continue;
|
|
821
|
+
seen.add(dedupeKey);
|
|
822
|
+
findings.push({
|
|
823
|
+
header: `cookie[${name}]`,
|
|
824
|
+
value_masked: maskSecret(val),
|
|
825
|
+
request_url: req.url,
|
|
826
|
+
confidence: scoreValue(val),
|
|
827
|
+
source: "cookie"
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const dedupeKey = `header:${lk}:${v.slice(0, 8)}`;
|
|
833
|
+
if (seen.has(dedupeKey)) continue;
|
|
834
|
+
seen.add(dedupeKey);
|
|
835
|
+
findings.push({
|
|
836
|
+
header: k,
|
|
837
|
+
value_masked: maskSecret(v),
|
|
838
|
+
request_url: req.url,
|
|
839
|
+
confidence: scoreValue(v),
|
|
840
|
+
source: "header"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const u = new URL(req.url);
|
|
845
|
+
for (const [k, v] of u.searchParams.entries()) {
|
|
846
|
+
if (!TOKEN_BODY_KEYS.test(k)) continue;
|
|
847
|
+
if (!v || v.length < 8) continue;
|
|
848
|
+
const dedupeKey = `query:${k}:${v.slice(0, 8)}`;
|
|
849
|
+
if (seen.has(dedupeKey)) continue;
|
|
850
|
+
seen.add(dedupeKey);
|
|
851
|
+
findings.push({
|
|
852
|
+
header: k,
|
|
853
|
+
value_masked: maskSecret(v),
|
|
854
|
+
request_url: req.url,
|
|
855
|
+
confidence: scoreValue(v) * .9,
|
|
856
|
+
source: "query"
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
} catch {}
|
|
860
|
+
if (req.postData) try {
|
|
861
|
+
const body = JSON.parse(req.postData);
|
|
862
|
+
if (body && typeof body === "object") for (const [k, v] of Object.entries(body)) {
|
|
863
|
+
if (!TOKEN_BODY_KEYS.test(k)) continue;
|
|
864
|
+
if (typeof v !== "string" || v.length < 8) continue;
|
|
865
|
+
const dedupeKey = `body:${k}:${v.slice(0, 8)}`;
|
|
866
|
+
if (seen.has(dedupeKey)) continue;
|
|
867
|
+
seen.add(dedupeKey);
|
|
868
|
+
findings.push({
|
|
869
|
+
header: k,
|
|
870
|
+
value_masked: maskSecret(v),
|
|
871
|
+
request_url: req.url,
|
|
872
|
+
confidence: scoreValue(v) * .85,
|
|
873
|
+
source: "body"
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
} catch {}
|
|
877
|
+
}
|
|
878
|
+
return findings.toSorted((a, b) => b.confidence - a.confidence);
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/server/domains/network/har.ts
|
|
882
|
+
/**
|
|
883
|
+
* HAR 1.2 builder — converts NetworkMonitor captured data to standard HAR format.
|
|
884
|
+
* Ref: http://www.softwareishard.com/blog/har-12-spec/
|
|
885
|
+
*/
|
|
886
|
+
function headersToHar(headers = {}) {
|
|
887
|
+
return Object.entries(headers).map(([name, value]) => ({
|
|
888
|
+
name,
|
|
889
|
+
value
|
|
890
|
+
}));
|
|
891
|
+
}
|
|
892
|
+
function parseCookies(cookieHeader) {
|
|
893
|
+
return cookieHeader.split(";").map((part) => {
|
|
894
|
+
const eq = part.indexOf("=");
|
|
895
|
+
if (eq === -1) return {
|
|
896
|
+
name: part.trim(),
|
|
897
|
+
value: ""
|
|
898
|
+
};
|
|
899
|
+
return {
|
|
900
|
+
name: part.slice(0, eq).trim(),
|
|
901
|
+
value: part.slice(eq + 1).trim()
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
function queryStringFromUrl(url) {
|
|
906
|
+
try {
|
|
907
|
+
const u = new URL(url);
|
|
908
|
+
return Array.from(u.searchParams.entries()).map(([name, value]) => ({
|
|
909
|
+
name,
|
|
910
|
+
value
|
|
911
|
+
}));
|
|
912
|
+
} catch {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function buildHar(params) {
|
|
917
|
+
const { requests, getResponse, getResponseBody, includeBodies, creatorVersion = "unknown" } = params;
|
|
918
|
+
const entries = [];
|
|
919
|
+
const bodyResults = /* @__PURE__ */ new Map();
|
|
920
|
+
if (includeBodies) {
|
|
921
|
+
const BODY_CONCURRENCY = NETWORK_HAR_BODY_CONCURRENCY;
|
|
922
|
+
for (let i = 0; i < requests.length; i += BODY_CONCURRENCY) {
|
|
923
|
+
const batch = requests.slice(i, i + BODY_CONCURRENCY);
|
|
924
|
+
const settled = await Promise.allSettled(batch.map(async (req) => {
|
|
925
|
+
try {
|
|
926
|
+
const bodyResult = await getResponseBody(req.requestId);
|
|
927
|
+
if (bodyResult) return {
|
|
928
|
+
requestId: req.requestId,
|
|
929
|
+
text: bodyResult.body
|
|
930
|
+
};
|
|
931
|
+
return {
|
|
932
|
+
requestId: req.requestId,
|
|
933
|
+
_bodyUnavailable: true
|
|
934
|
+
};
|
|
935
|
+
} catch {
|
|
936
|
+
return {
|
|
937
|
+
requestId: req.requestId,
|
|
938
|
+
_bodyUnavailable: true
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
}));
|
|
942
|
+
for (const result of settled) if (result.status === "fulfilled") {
|
|
943
|
+
const val = result.value;
|
|
944
|
+
bodyResults.set(val.requestId, "_bodyUnavailable" in val ? { _bodyUnavailable: true } : { text: val.text });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const req of requests) {
|
|
949
|
+
const res = getResponse(req.requestId);
|
|
950
|
+
const startedDateTime = req.timestamp ? (/* @__PURE__ */ new Date(req.timestamp * 1e3)).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
951
|
+
const bodyContent = includeBodies ? bodyResults.get(req.requestId) ?? { _bodyUnavailable: true } : {};
|
|
952
|
+
const postData = req.postData ? {
|
|
953
|
+
mimeType: req.headers?.["content-type"] ?? "application/octet-stream",
|
|
954
|
+
text: req.postData
|
|
955
|
+
} : void 0;
|
|
956
|
+
const reqCookieHeader = req.headers?.["cookie"] ?? "";
|
|
957
|
+
const resCookieHeader = res?.headers?.["set-cookie"] ?? "";
|
|
958
|
+
const entry = {
|
|
959
|
+
startedDateTime,
|
|
960
|
+
time: res?.timing?.receiveHeadersEnd ?? 0,
|
|
961
|
+
request: {
|
|
962
|
+
method: req.method,
|
|
963
|
+
url: req.url,
|
|
964
|
+
httpVersion: "HTTP/1.1",
|
|
965
|
+
headers: headersToHar(req.headers),
|
|
966
|
+
queryString: queryStringFromUrl(req.url),
|
|
967
|
+
cookies: reqCookieHeader ? parseCookies(reqCookieHeader) : [],
|
|
968
|
+
headersSize: -1,
|
|
969
|
+
bodySize: req.postData ? req.postData.length : 0,
|
|
970
|
+
...postData ? { postData } : {}
|
|
971
|
+
},
|
|
972
|
+
response: {
|
|
973
|
+
status: res?.status ?? 0,
|
|
974
|
+
statusText: res?.statusText ?? "",
|
|
975
|
+
httpVersion: "HTTP/1.1",
|
|
976
|
+
headers: headersToHar(res?.headers),
|
|
977
|
+
cookies: resCookieHeader ? parseCookies(resCookieHeader) : [],
|
|
978
|
+
content: {
|
|
979
|
+
size: bodyContent.text ? bodyContent.text.length : -1,
|
|
980
|
+
mimeType: res?.mimeType ?? "application/octet-stream",
|
|
981
|
+
...bodyContent
|
|
982
|
+
},
|
|
983
|
+
redirectURL: res?.headers?.["location"] ?? "",
|
|
984
|
+
headersSize: -1,
|
|
985
|
+
bodySize: bodyContent.text ? bodyContent.text.length : -1
|
|
986
|
+
},
|
|
987
|
+
cache: {},
|
|
988
|
+
timings: {
|
|
989
|
+
send: 0,
|
|
990
|
+
wait: res?.timing?.receiveHeadersEnd ?? 0,
|
|
991
|
+
receive: 0
|
|
992
|
+
},
|
|
993
|
+
_requestId: req.requestId
|
|
994
|
+
};
|
|
995
|
+
entries.push(entry);
|
|
996
|
+
}
|
|
997
|
+
return { log: {
|
|
998
|
+
version: "1.2",
|
|
999
|
+
creator: {
|
|
1000
|
+
name: "jshookmcp",
|
|
1001
|
+
version: creatorVersion
|
|
1002
|
+
},
|
|
1003
|
+
entries
|
|
1004
|
+
} };
|
|
1005
|
+
}
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region src/server/domains/network/replay.ts
|
|
1008
|
+
/**
|
|
1009
|
+
* Request Replay — rebuilds and re-sends a captured network request
|
|
1010
|
+
* with optional header/body/method/URL overrides.
|
|
1011
|
+
*
|
|
1012
|
+
* Security: dryRun defaults to true to prevent accidental side-effects.
|
|
1013
|
+
* Always sanitize headers that would conflict (host, content-length, transfer-encoding).
|
|
1014
|
+
* SSRF guard resolves DNS before checking to defeat rebinding attacks.
|
|
1015
|
+
*/
|
|
1016
|
+
const STRIPPED_HEADERS = new Set([
|
|
1017
|
+
"host",
|
|
1018
|
+
"content-length",
|
|
1019
|
+
"transfer-encoding",
|
|
1020
|
+
"connection",
|
|
1021
|
+
"keep-alive",
|
|
1022
|
+
"proxy-authenticate",
|
|
1023
|
+
"proxy-authorization",
|
|
1024
|
+
"te",
|
|
1025
|
+
"trailers",
|
|
1026
|
+
"upgrade"
|
|
1027
|
+
]);
|
|
1028
|
+
const DANGEROUS_KEYS = new Set([
|
|
1029
|
+
"__proto__",
|
|
1030
|
+
"constructor",
|
|
1031
|
+
"prototype"
|
|
1032
|
+
]);
|
|
1033
|
+
function sanitizeHeaders(headers) {
|
|
1034
|
+
const out = Object.create(null);
|
|
1035
|
+
for (const [k, v] of Object.entries(headers)) if (!STRIPPED_HEADERS.has(k.toLowerCase()) && !DANGEROUS_KEYS.has(k)) out[k] = v;
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
async function replayRequest(base, args, maxBodyBytes = 512e3) {
|
|
1039
|
+
const url = args.urlOverride ?? base.url;
|
|
1040
|
+
const method = (args.methodOverride ?? base.method).toUpperCase();
|
|
1041
|
+
const mergedHeaders = sanitizeHeaders({
|
|
1042
|
+
...base.headers,
|
|
1043
|
+
...args.headerPatch
|
|
1044
|
+
});
|
|
1045
|
+
const body = args.bodyPatch !== void 0 ? args.bodyPatch : base.postData;
|
|
1046
|
+
const authorizationPolicy = createNetworkAuthorizationPolicy(args.authorization);
|
|
1047
|
+
const allowLegacyLocalSsrf = !authorizationPolicy && isLocalSsrfBypassEnabled();
|
|
1048
|
+
if (authorizationPolicy && (authorizationPolicy.allowPrivateNetwork || authorizationPolicy.allowInsecureHttp) && !hasAuthorizedTargets(authorizationPolicy)) throw new Error("Replay authorization must include at least one allowed host or CIDR when enabling private network or insecure HTTP access.");
|
|
1049
|
+
if (isNetworkAuthorizationExpired(authorizationPolicy)) throw new Error("Replay authorization expired before the request was executed.");
|
|
1050
|
+
const isPrivateTargetAllowed = (target) => {
|
|
1051
|
+
if (allowLegacyLocalSsrf) return true;
|
|
1052
|
+
return authorizationPolicy?.allowPrivateNetwork === true && isAuthorizedNetworkTarget(authorizationPolicy, target);
|
|
1053
|
+
};
|
|
1054
|
+
const isInsecureHttpAllowed = (target) => {
|
|
1055
|
+
if (target.parsedUrl.protocol !== "http:") return true;
|
|
1056
|
+
if (allowLegacyLocalSsrf) return true;
|
|
1057
|
+
if (isLoopbackHost(target.hostname)) return true;
|
|
1058
|
+
return authorizationPolicy?.allowInsecureHttp === true && isAuthorizedNetworkTarget(authorizationPolicy, target);
|
|
1059
|
+
};
|
|
1060
|
+
const resolvePinned = async (targetUrl) => {
|
|
1061
|
+
let target;
|
|
1062
|
+
try {
|
|
1063
|
+
target = await resolveNetworkTarget(targetUrl);
|
|
1064
|
+
} catch {
|
|
1065
|
+
throw new Error(`Replay blocked: DNS resolution failed for "${targetUrl}"`);
|
|
1066
|
+
}
|
|
1067
|
+
if (!isInsecureHttpAllowed(target)) throw new Error(`Replay blocked: insecure HTTP is only allowed for loopback or explicitly authorized targets, got "${targetUrl}"`);
|
|
1068
|
+
const hostnameIsPrivate = isPrivateHost(target.hostname);
|
|
1069
|
+
const resolvedAddressIsPrivate = isPrivateHost(target.resolvedAddress ?? "");
|
|
1070
|
+
if ((hostnameIsPrivate || resolvedAddressIsPrivate) && !isPrivateTargetAllowed(target)) {
|
|
1071
|
+
if (!hostnameIsPrivate && resolvedAddressIsPrivate && target.resolvedAddress) throw new Error(`Replay blocked: "${targetUrl}" resolved to private IP ${target.resolvedAddress}`);
|
|
1072
|
+
throw new Error(`Replay blocked: target URL "${targetUrl}" resolves to a private/reserved address.`);
|
|
1073
|
+
}
|
|
1074
|
+
if (target.parsedUrl.protocol === "https:" || target.isIpLiteral) return {
|
|
1075
|
+
pinnedUrl: targetUrl,
|
|
1076
|
+
originalHost: target.parsedUrl.host,
|
|
1077
|
+
target
|
|
1078
|
+
};
|
|
1079
|
+
const originalHost = target.parsedUrl.host;
|
|
1080
|
+
target.parsedUrl.hostname = target.resolvedAddress && target.resolvedAddress.includes(":") ? `[${target.resolvedAddress}]` : target.resolvedAddress ?? target.hostname;
|
|
1081
|
+
return {
|
|
1082
|
+
pinnedUrl: target.parsedUrl.toString(),
|
|
1083
|
+
originalHost,
|
|
1084
|
+
target
|
|
1085
|
+
};
|
|
1086
|
+
};
|
|
1087
|
+
if (args.dryRun !== false) {
|
|
1088
|
+
if (await isSsrfTarget(url, args.authorization)) throw new Error(`Replay blocked: target URL "${url}" resolves to a private/reserved address.`);
|
|
1089
|
+
const dryRunTarget = await resolveNetworkTarget(url).catch(() => null);
|
|
1090
|
+
if (dryRunTarget && !isInsecureHttpAllowed(dryRunTarget)) throw new Error(`Replay blocked: insecure HTTP is only allowed for loopback or explicitly authorized targets, got "${url}"`);
|
|
1091
|
+
return {
|
|
1092
|
+
dryRun: true,
|
|
1093
|
+
preview: {
|
|
1094
|
+
url,
|
|
1095
|
+
method,
|
|
1096
|
+
headers: mergedHeaders,
|
|
1097
|
+
body
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const controller = new AbortController();
|
|
1102
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs ?? 3e4);
|
|
1103
|
+
const MAX_REDIRECTS = NETWORK_REPLAY_MAX_REDIRECTS;
|
|
1104
|
+
try {
|
|
1105
|
+
let currentUrl = url;
|
|
1106
|
+
let currentMethod = method;
|
|
1107
|
+
let currentBody = body;
|
|
1108
|
+
let resp;
|
|
1109
|
+
for (let hop = 0; hop < MAX_REDIRECTS; hop++) {
|
|
1110
|
+
const { pinnedUrl, originalHost, target } = await resolvePinned(currentUrl);
|
|
1111
|
+
const hopHeaders = { ...mergedHeaders };
|
|
1112
|
+
if (target.parsedUrl.protocol === "http:" && target.resolvedAddress && !target.isIpLiteral) hopHeaders.Host = originalHost;
|
|
1113
|
+
resp = await fetch(pinnedUrl, {
|
|
1114
|
+
method: currentMethod,
|
|
1115
|
+
headers: hopHeaders,
|
|
1116
|
+
body: currentMethod !== "GET" && currentMethod !== "HEAD" ? currentBody : void 0,
|
|
1117
|
+
signal: controller.signal,
|
|
1118
|
+
redirect: "manual"
|
|
1119
|
+
});
|
|
1120
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
1121
|
+
const location = resp.headers.get("location");
|
|
1122
|
+
if (!location) break;
|
|
1123
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
1124
|
+
if (resp.status === 301 || resp.status === 302 || resp.status === 303) {
|
|
1125
|
+
currentMethod = "GET";
|
|
1126
|
+
currentBody = void 0;
|
|
1127
|
+
}
|
|
1128
|
+
delete mergedHeaders["Host"];
|
|
1129
|
+
delete mergedHeaders["host"];
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
if (resp.status >= 300 && resp.status < 400) throw new Error(`Replay blocked: too many redirects (>${MAX_REDIRECTS})`);
|
|
1135
|
+
const responseHeaders = {};
|
|
1136
|
+
resp.headers.forEach((v, k) => {
|
|
1137
|
+
responseHeaders[k] = v;
|
|
1138
|
+
});
|
|
1139
|
+
const rawText = await resp.text();
|
|
1140
|
+
const bodyTruncated = rawText.length > maxBodyBytes;
|
|
1141
|
+
const bodyOut = bodyTruncated ? rawText.slice(0, maxBodyBytes) : rawText;
|
|
1142
|
+
return {
|
|
1143
|
+
dryRun: false,
|
|
1144
|
+
status: resp.status,
|
|
1145
|
+
statusText: resp.statusText,
|
|
1146
|
+
headers: responseHeaders,
|
|
1147
|
+
body: bodyOut,
|
|
1148
|
+
bodyTruncated,
|
|
1149
|
+
requestId: args.requestId
|
|
1150
|
+
};
|
|
1151
|
+
} finally {
|
|
1152
|
+
clearTimeout(timeoutId);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/server/domains/network/handlers/replay-handlers.ts
|
|
1157
|
+
/**
|
|
1158
|
+
* Replay and analysis handlers — auth extraction, HAR export, request replay.
|
|
1159
|
+
*
|
|
1160
|
+
* Extracted from AdvancedToolHandlersRuntime (handlers.impl.core.runtime.replay.ts).
|
|
1161
|
+
*/
|
|
1162
|
+
const isReplayableRequest = (value) => {
|
|
1163
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1164
|
+
const record = value;
|
|
1165
|
+
return typeof record.requestId === "string" && typeof record.url === "string" && typeof record.method === "string";
|
|
1166
|
+
};
|
|
1167
|
+
const parseStringArray$1 = (value, field) => {
|
|
1168
|
+
if (value === void 0) return [];
|
|
1169
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${field} must be an array of strings`);
|
|
1170
|
+
return value.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1171
|
+
};
|
|
1172
|
+
const parseOptionalString$1 = (value, field) => {
|
|
1173
|
+
if (value === void 0) return;
|
|
1174
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
1175
|
+
const trimmed = value.trim();
|
|
1176
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1177
|
+
};
|
|
1178
|
+
const parseOptionalBoolean$1 = (value, field) => {
|
|
1179
|
+
if (value === void 0) return;
|
|
1180
|
+
if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
|
|
1181
|
+
return value;
|
|
1182
|
+
};
|
|
1183
|
+
const decodeAuthorizationCapability = (capability, requestId) => {
|
|
1184
|
+
if (typeof capability !== "string" || capability.trim().length === 0) throw new Error("authorizationCapability must be a non-empty base64url string");
|
|
1185
|
+
let parsed;
|
|
1186
|
+
try {
|
|
1187
|
+
parsed = JSON.parse(Buffer.from(capability, "base64url").toString("utf8"));
|
|
1188
|
+
} catch {
|
|
1189
|
+
throw new Error("authorizationCapability must be valid base64url-encoded JSON");
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("authorizationCapability payload must be an object");
|
|
1192
|
+
const payload = parsed;
|
|
1193
|
+
if (payload.version !== void 0 && payload.version !== 1) throw new Error(`authorizationCapability version ${String(payload.version)} is not supported`);
|
|
1194
|
+
if (payload.requestId !== requestId) throw new Error("authorizationCapability requestId does not match the replay requestId");
|
|
1195
|
+
return payload;
|
|
1196
|
+
};
|
|
1197
|
+
const parseReplayAuthorization = (args, requestId) => {
|
|
1198
|
+
const authorizationArg = args.authorization;
|
|
1199
|
+
const capabilityArg = args.authorizationCapability;
|
|
1200
|
+
if (authorizationArg !== void 0 && capabilityArg !== void 0) throw new Error("Provide either authorization or authorizationCapability, not both");
|
|
1201
|
+
let source;
|
|
1202
|
+
if (authorizationArg !== void 0) {
|
|
1203
|
+
if (typeof authorizationArg !== "object" || authorizationArg === null || Array.isArray(authorizationArg)) throw new Error("authorization must be an object");
|
|
1204
|
+
source = authorizationArg;
|
|
1205
|
+
} else if (capabilityArg !== void 0) source = decodeAuthorizationCapability(capabilityArg, requestId);
|
|
1206
|
+
else return;
|
|
1207
|
+
const allowedHosts = parseStringArray$1(source.allowedHosts, "authorization.allowedHosts");
|
|
1208
|
+
const allowedCidrs = parseStringArray$1(source.allowedCidrs, "authorization.allowedCidrs");
|
|
1209
|
+
const allowPrivateNetwork = parseOptionalBoolean$1(source.allowPrivateNetwork, "authorization.allowPrivateNetwork");
|
|
1210
|
+
const allowInsecureHttp = parseOptionalBoolean$1(source.allowInsecureHttp, "authorization.allowInsecureHttp");
|
|
1211
|
+
const expiresAt = parseOptionalString$1(source.expiresAt, "authorization.expiresAt");
|
|
1212
|
+
const reason = parseOptionalString$1(source.reason, "authorization.reason");
|
|
1213
|
+
const authorization = {};
|
|
1214
|
+
if (allowedHosts.length > 0) authorization.allowedHosts = allowedHosts;
|
|
1215
|
+
if (allowedCidrs.length > 0) authorization.allowedCidrs = allowedCidrs;
|
|
1216
|
+
if (allowPrivateNetwork !== void 0) authorization.allowPrivateNetwork = allowPrivateNetwork;
|
|
1217
|
+
if (allowInsecureHttp !== void 0) authorization.allowInsecureHttp = allowInsecureHttp;
|
|
1218
|
+
if (expiresAt !== void 0) authorization.expiresAt = expiresAt;
|
|
1219
|
+
if (reason !== void 0) authorization.reason = reason;
|
|
1220
|
+
return authorization;
|
|
1221
|
+
};
|
|
1222
|
+
var ReplayHandlers = class {
|
|
1223
|
+
detailedDataManager = getDetailedDataManager();
|
|
1224
|
+
constructor(deps) {
|
|
1225
|
+
this.deps = deps;
|
|
1226
|
+
}
|
|
1227
|
+
async handleNetworkExtractAuth(args) {
|
|
1228
|
+
try {
|
|
1229
|
+
const minConfidence = parseNumberArg(args.minConfidence, { defaultValue: .4 });
|
|
1230
|
+
const requests = this.deps.consoleMonitor.getNetworkRequests();
|
|
1231
|
+
if (requests.length === 0) return R.fail("No captured requests found. Call network_enable then page_navigate first.").json();
|
|
1232
|
+
const findings = extractAuthFromRequests(requests).filter((f) => f.confidence >= minConfidence);
|
|
1233
|
+
return R.ok().merge({
|
|
1234
|
+
scannedRequests: requests.length,
|
|
1235
|
+
found: findings.length,
|
|
1236
|
+
findings,
|
|
1237
|
+
note: "Values are masked (first 6 + last 4 chars). Use network_replay_request to test with actual values."
|
|
1238
|
+
}).json();
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
return R.fail(error).json();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
async handleNetworkExportHar(args) {
|
|
1244
|
+
try {
|
|
1245
|
+
const outputPath = args.outputPath;
|
|
1246
|
+
const includeBodies = parseBooleanArg(args.includeBodies, false);
|
|
1247
|
+
let resolvedOutputPath;
|
|
1248
|
+
if (outputPath) {
|
|
1249
|
+
const path = await import("node:path");
|
|
1250
|
+
const fsDynamic = await import("node:fs/promises");
|
|
1251
|
+
const resolved = path.resolve(outputPath);
|
|
1252
|
+
const cwd = await fsDynamic.realpath(process.cwd());
|
|
1253
|
+
const tmpDir = await fsDynamic.realpath((await import("node:os")).tmpdir());
|
|
1254
|
+
const parentDir = path.dirname(resolved);
|
|
1255
|
+
let realParent;
|
|
1256
|
+
try {
|
|
1257
|
+
realParent = await fsDynamic.realpath(parentDir);
|
|
1258
|
+
} catch {
|
|
1259
|
+
realParent = parentDir;
|
|
1260
|
+
}
|
|
1261
|
+
const realPath = path.join(realParent, path.basename(resolved));
|
|
1262
|
+
const inCwd = realPath === cwd || realPath.startsWith(cwd + path.sep);
|
|
1263
|
+
const inTmp = realPath === tmpDir || realPath.startsWith(tmpDir + path.sep);
|
|
1264
|
+
if (!inCwd && !inTmp) return R.fail("outputPath must be within the current working directory or system temp dir.").json();
|
|
1265
|
+
resolvedOutputPath = realPath;
|
|
1266
|
+
}
|
|
1267
|
+
const requests = this.deps.consoleMonitor.getNetworkRequests();
|
|
1268
|
+
if (requests.length === 0) return R.fail("No captured requests to export. Call network_enable then page_navigate first.").json();
|
|
1269
|
+
const getResponse = (id) => this.deps.consoleMonitor.getNetworkActivity(id)?.response;
|
|
1270
|
+
const har = await buildHar({
|
|
1271
|
+
requests,
|
|
1272
|
+
getResponse,
|
|
1273
|
+
getResponseBody: async (id) => {
|
|
1274
|
+
try {
|
|
1275
|
+
return await this.deps.consoleMonitor.getResponseBody(id);
|
|
1276
|
+
} catch {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
includeBodies,
|
|
1281
|
+
creatorVersion: "1.0.0"
|
|
1282
|
+
});
|
|
1283
|
+
if (resolvedOutputPath) {
|
|
1284
|
+
try {
|
|
1285
|
+
if ((await promises.lstat(resolvedOutputPath)).isSymbolicLink()) return R.fail("outputPath must not be a symbolic link.").json();
|
|
1286
|
+
} catch {}
|
|
1287
|
+
await promises.writeFile(resolvedOutputPath, JSON.stringify(har, null, 2), "utf-8");
|
|
1288
|
+
return R.ok().merge({
|
|
1289
|
+
message: `HAR exported to ${resolvedOutputPath}`,
|
|
1290
|
+
entryCount: har.log.entries.length,
|
|
1291
|
+
outputPath: resolvedOutputPath
|
|
1292
|
+
}).json();
|
|
1293
|
+
}
|
|
1294
|
+
const result = this.detailedDataManager.smartHandle({
|
|
1295
|
+
entryCount: har.log.entries.length,
|
|
1296
|
+
har
|
|
1297
|
+
}, 51200);
|
|
1298
|
+
return R.ok().merge(result).json();
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
return R.fail(error).json();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async handleNetworkReplayRequest(args) {
|
|
1304
|
+
try {
|
|
1305
|
+
const requestId = args.requestId;
|
|
1306
|
+
if (!requestId) return R.fail("requestId is required").json();
|
|
1307
|
+
const base = this.deps.consoleMonitor.getNetworkRequests().find((request) => isReplayableRequest(request) && request.requestId === requestId);
|
|
1308
|
+
if (!base) return R.fail(`Request ${requestId} not found in captured requests`).merge({ hint: "Use network_get_requests to list available requestIds" }).json();
|
|
1309
|
+
const authorization = parseReplayAuthorization(args, requestId);
|
|
1310
|
+
const result = await replayRequest(base, {
|
|
1311
|
+
requestId,
|
|
1312
|
+
headerPatch: args.headerPatch,
|
|
1313
|
+
bodyPatch: args.bodyPatch,
|
|
1314
|
+
methodOverride: args.methodOverride,
|
|
1315
|
+
urlOverride: args.urlOverride,
|
|
1316
|
+
timeoutMs: args.timeoutMs,
|
|
1317
|
+
dryRun: args.dryRun !== false,
|
|
1318
|
+
authorization
|
|
1319
|
+
});
|
|
1320
|
+
return R.ok().merge(result).json();
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
return R.fail(error).json();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
//#endregion
|
|
1327
|
+
//#region src/server/domains/network/handlers/intercept-handlers.ts
|
|
1328
|
+
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
1329
|
+
var InterceptHandlers = class {
|
|
1330
|
+
constructor(deps) {
|
|
1331
|
+
this.deps = deps;
|
|
1332
|
+
}
|
|
1333
|
+
async handleNetworkInterceptResponse(args) {
|
|
1334
|
+
try {
|
|
1335
|
+
const rules = this.parseInterceptRules(args);
|
|
1336
|
+
if (rules.length === 0) return R.fail("No valid rules provided. Provide either \"urlPattern\" (single) or \"rules\" array (batch).").merge({ usage: {
|
|
1337
|
+
single: {
|
|
1338
|
+
urlPattern: "*api/status*",
|
|
1339
|
+
responseCode: 200,
|
|
1340
|
+
responseBody: "{\"status\":\"active\"}"
|
|
1341
|
+
},
|
|
1342
|
+
batch: { rules: [{
|
|
1343
|
+
urlPattern: "*api/status*",
|
|
1344
|
+
responseBody: "{\"status\":\"active\"}"
|
|
1345
|
+
}] }
|
|
1346
|
+
} }).json();
|
|
1347
|
+
const createdRules = await this.deps.consoleMonitor.enableFetchIntercept(rules);
|
|
1348
|
+
const status = this.deps.consoleMonitor.getFetchInterceptStatus();
|
|
1349
|
+
emitEvent(this.deps.eventBus, "network:intercept_started", {
|
|
1350
|
+
interceptType: "fetch",
|
|
1351
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1352
|
+
});
|
|
1353
|
+
return R.ok().merge({
|
|
1354
|
+
message: `Added ${createdRules.length} interception rule(s)`,
|
|
1355
|
+
createdRules: createdRules.map((r) => ({
|
|
1356
|
+
id: r.id,
|
|
1357
|
+
urlPattern: r.urlPattern,
|
|
1358
|
+
stage: r.stage,
|
|
1359
|
+
responseCode: r.responseCode
|
|
1360
|
+
})),
|
|
1361
|
+
totalActiveRules: status.rules.length,
|
|
1362
|
+
hint: "Use network_intercept(action: \"list\") to see all rules and hit counts. Use network_intercept(action: \"disable\") to remove rules."
|
|
1363
|
+
}).json();
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
return R.fail(error instanceof Error ? error.message : String(error)).merge({ hint: "Ensure browser is launched and a page is active before enabling interception." }).json();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async handleNetworkInterceptList(_args) {
|
|
1369
|
+
const status = this.deps.consoleMonitor.getFetchInterceptStatus();
|
|
1370
|
+
return R.ok().merge(status).merge({ hint: status.rules.length > 0 ? "Use network_intercept(action: \"disable\", ruleId) to remove a specific rule, or network_intercept(action: \"disable\", all: true) to remove all." : "No active interception rules. Use network_intercept(action: \"add\") to add rules." }).json();
|
|
1371
|
+
}
|
|
1372
|
+
async handleNetworkInterceptDisable(args) {
|
|
1373
|
+
const ruleId = typeof args.ruleId === "string" ? args.ruleId : void 0;
|
|
1374
|
+
const all = args.all === true;
|
|
1375
|
+
if (!ruleId && !all) return R.fail("Provide either \"ruleId\" to remove a specific rule, or \"all\": true to disable all.").json();
|
|
1376
|
+
try {
|
|
1377
|
+
if (all) {
|
|
1378
|
+
const result = await this.deps.consoleMonitor.disableFetchIntercept();
|
|
1379
|
+
return R.ok().merge({
|
|
1380
|
+
message: `Disabled all interception. Removed ${result.removedRules} rule(s).`,
|
|
1381
|
+
removedRules: result.removedRules
|
|
1382
|
+
}).json();
|
|
1383
|
+
}
|
|
1384
|
+
const removed = await this.deps.consoleMonitor.removeFetchInterceptRule(ruleId);
|
|
1385
|
+
const status = this.deps.consoleMonitor.getFetchInterceptStatus();
|
|
1386
|
+
return R.ok().merge({
|
|
1387
|
+
success: removed,
|
|
1388
|
+
message: removed ? `Rule ${ruleId} removed.` : `Rule ${ruleId} not found.`,
|
|
1389
|
+
remainingRules: status.rules.length
|
|
1390
|
+
}).json();
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
parseInterceptRules(args) {
|
|
1396
|
+
const rules = [];
|
|
1397
|
+
if (Array.isArray(args.rules)) {
|
|
1398
|
+
for (const rawRule of args.rules) if (isObjectRecord(rawRule) && typeof rawRule.urlPattern === "string") rules.push(this.toInterceptRule(rawRule));
|
|
1399
|
+
} else if (typeof args.urlPattern === "string") rules.push(this.toInterceptRule(args));
|
|
1400
|
+
return rules;
|
|
1401
|
+
}
|
|
1402
|
+
toInterceptRule(source) {
|
|
1403
|
+
return {
|
|
1404
|
+
urlPattern: source.urlPattern,
|
|
1405
|
+
urlPatternType: source.urlPatternType === "regex" ? "regex" : "glob",
|
|
1406
|
+
stage: source.stage === "Request" ? "Request" : "Response",
|
|
1407
|
+
responseCode: typeof source.responseCode === "number" ? source.responseCode : 200,
|
|
1408
|
+
responseHeaders: isObjectRecord(source.responseHeaders) ? source.responseHeaders : void 0,
|
|
1409
|
+
responseBody: typeof source.responseBody === "string" ? source.responseBody : typeof source.responseBody === "object" ? JSON.stringify(source.responseBody) : void 0
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src/utils/BufferChain.ts
|
|
1415
|
+
/**
|
|
1416
|
+
* Zero-copy buffer chain — avoids repeated Buffer.concat allocations.
|
|
1417
|
+
*
|
|
1418
|
+
* Appends chunks without copying. Materializes into a single Buffer only
|
|
1419
|
+
* when `toBuffer()` is called. Tracks total byte length for size limits.
|
|
1420
|
+
*/
|
|
1421
|
+
var BufferChain = class {
|
|
1422
|
+
chunks = [];
|
|
1423
|
+
totalLength = 0;
|
|
1424
|
+
/** Number of bytes accumulated so far. */
|
|
1425
|
+
get length() {
|
|
1426
|
+
return this.totalLength;
|
|
1427
|
+
}
|
|
1428
|
+
/** Append a chunk without copying. */
|
|
1429
|
+
append(chunk) {
|
|
1430
|
+
if (chunk.length === 0) return;
|
|
1431
|
+
this.chunks.push(chunk);
|
|
1432
|
+
this.totalLength += chunk.length;
|
|
1433
|
+
}
|
|
1434
|
+
/** Materialize all chunks into a single Buffer. */
|
|
1435
|
+
toBuffer() {
|
|
1436
|
+
if (this.chunks.length === 0) return Buffer.alloc(0);
|
|
1437
|
+
if (this.chunks.length === 1) return this.chunks[0];
|
|
1438
|
+
const result = Buffer.concat(this.chunks, this.totalLength);
|
|
1439
|
+
this.chunks = [result];
|
|
1440
|
+
return result;
|
|
1441
|
+
}
|
|
1442
|
+
/** Reset the chain, releasing all chunk references. */
|
|
1443
|
+
reset() {
|
|
1444
|
+
this.chunks = [];
|
|
1445
|
+
this.totalLength = 0;
|
|
1446
|
+
}
|
|
1447
|
+
/** Whether any data has been accumulated. */
|
|
1448
|
+
get isEmpty() {
|
|
1449
|
+
return this.totalLength === 0;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
//#endregion
|
|
1453
|
+
//#region src/server/domains/network/http-raw.ts
|
|
1454
|
+
const HEADER_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
1455
|
+
const TEXTUAL_CONTENT_TYPE_RE = /^(?:text\/|application\/(?:json|ld\+json|xml|xhtml\+xml|javascript|x-javascript|problem\+json|problem\+xml|graphql-response\+json|x-www-form-urlencoded)|image\/svg\+xml)/i;
|
|
1456
|
+
function assertNoLineBreak(value, field) {
|
|
1457
|
+
if (value.includes("\r") || value.includes("\n")) throw new Error(`${field} must not contain CR or LF characters`);
|
|
1458
|
+
}
|
|
1459
|
+
function hasHeader(headers, headerName) {
|
|
1460
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === headerName.toLowerCase());
|
|
1461
|
+
}
|
|
1462
|
+
function findHeaderValue(headers, headerName) {
|
|
1463
|
+
return headers.find((header) => header.name.toLowerCase() === headerName.toLowerCase())?.value ?? null;
|
|
1464
|
+
}
|
|
1465
|
+
function isBodylessResponse(statusCode, requestMethod) {
|
|
1466
|
+
return requestMethod?.toUpperCase() === "HEAD" || statusCode >= 100 && statusCode < 200 || statusCode === 204 || statusCode === 304;
|
|
1467
|
+
}
|
|
1468
|
+
function findHeaderTerminator(buffer) {
|
|
1469
|
+
const crlf = buffer.indexOf("\r\n\r\n");
|
|
1470
|
+
if (crlf >= 0) return crlf + 4;
|
|
1471
|
+
const lf = buffer.indexOf("\n\n");
|
|
1472
|
+
if (lf >= 0) return lf + 2;
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
function findLineBreak(buffer, start) {
|
|
1476
|
+
const lfIndex = buffer.indexOf(10, start);
|
|
1477
|
+
if (lfIndex < 0) return null;
|
|
1478
|
+
return {
|
|
1479
|
+
lineEnd: lfIndex > start && buffer[lfIndex - 1] === 13 ? lfIndex - 1 : lfIndex,
|
|
1480
|
+
nextOffset: lfIndex + 1
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function decodeChunkedBody(buffer) {
|
|
1484
|
+
const chunks = [];
|
|
1485
|
+
let offset = 0;
|
|
1486
|
+
while (offset < buffer.length) {
|
|
1487
|
+
const line = findLineBreak(buffer, offset);
|
|
1488
|
+
if (!line) return {
|
|
1489
|
+
complete: false,
|
|
1490
|
+
consumedBytes: offset,
|
|
1491
|
+
body: Buffer.concat(chunks)
|
|
1492
|
+
};
|
|
1493
|
+
const sizeToken = buffer.subarray(offset, line.lineEnd).toString("latin1").trim().split(";", 1)[0]?.trim() ?? "";
|
|
1494
|
+
const chunkSize = Number.parseInt(sizeToken, 16);
|
|
1495
|
+
if (!Number.isFinite(chunkSize) || chunkSize < 0) return {
|
|
1496
|
+
complete: false,
|
|
1497
|
+
consumedBytes: offset,
|
|
1498
|
+
body: Buffer.concat(chunks)
|
|
1499
|
+
};
|
|
1500
|
+
if (chunkSize === 0) {
|
|
1501
|
+
const trailerSection = buffer.subarray(line.nextOffset);
|
|
1502
|
+
const trailerEnd = findHeaderTerminator(trailerSection);
|
|
1503
|
+
if (trailerEnd !== null) return {
|
|
1504
|
+
complete: true,
|
|
1505
|
+
consumedBytes: line.nextOffset + trailerEnd,
|
|
1506
|
+
body: Buffer.concat(chunks)
|
|
1507
|
+
};
|
|
1508
|
+
if (trailerSection.length >= 2 && trailerSection[0] === 13 && trailerSection[1] === 10) return {
|
|
1509
|
+
complete: true,
|
|
1510
|
+
consumedBytes: line.nextOffset + 2,
|
|
1511
|
+
body: Buffer.concat(chunks)
|
|
1512
|
+
};
|
|
1513
|
+
if (trailerSection.length >= 1 && trailerSection[0] === 10) return {
|
|
1514
|
+
complete: true,
|
|
1515
|
+
consumedBytes: line.nextOffset + 1,
|
|
1516
|
+
body: Buffer.concat(chunks)
|
|
1517
|
+
};
|
|
1518
|
+
return {
|
|
1519
|
+
complete: false,
|
|
1520
|
+
consumedBytes: offset,
|
|
1521
|
+
body: Buffer.concat(chunks)
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
const dataStart = line.nextOffset;
|
|
1525
|
+
const dataEnd = dataStart + chunkSize;
|
|
1526
|
+
if (dataEnd > buffer.length) return {
|
|
1527
|
+
complete: false,
|
|
1528
|
+
consumedBytes: offset,
|
|
1529
|
+
body: Buffer.concat(chunks)
|
|
1530
|
+
};
|
|
1531
|
+
chunks.push(buffer.subarray(dataStart, dataEnd));
|
|
1532
|
+
const afterChunkLine = findLineBreak(buffer, dataEnd);
|
|
1533
|
+
if (!afterChunkLine || afterChunkLine.lineEnd !== dataEnd) return {
|
|
1534
|
+
complete: false,
|
|
1535
|
+
consumedBytes: offset,
|
|
1536
|
+
body: Buffer.concat(chunks)
|
|
1537
|
+
};
|
|
1538
|
+
offset = afterChunkLine.nextOffset;
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
complete: false,
|
|
1542
|
+
consumedBytes: offset,
|
|
1543
|
+
body: Buffer.concat(chunks)
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function buildHeaderSection(headers) {
|
|
1547
|
+
const entries = Object.entries(headers);
|
|
1548
|
+
if (entries.length === 0) return "";
|
|
1549
|
+
return `${entries.map(([name, value]) => `${name}: ${value}`).join("\r\n")}\r\n`;
|
|
1550
|
+
}
|
|
1551
|
+
function buildHttpRequest(input) {
|
|
1552
|
+
const method = input.method.trim().toUpperCase();
|
|
1553
|
+
const target = input.target.trim();
|
|
1554
|
+
const httpVersion = input.httpVersion ?? "1.1";
|
|
1555
|
+
if (!HEADER_NAME_RE.test(method)) throw new Error("method must be a valid HTTP token");
|
|
1556
|
+
if (target.length === 0) throw new Error("target is required");
|
|
1557
|
+
assertNoLineBreak(target, "target");
|
|
1558
|
+
if (httpVersion !== "1.0" && httpVersion !== "1.1") throw new Error("httpVersion must be either \"1.0\" or \"1.1\"");
|
|
1559
|
+
const headers = {};
|
|
1560
|
+
for (const [name, value] of Object.entries(input.headers ?? {})) {
|
|
1561
|
+
if (!HEADER_NAME_RE.test(name)) throw new Error(`Invalid HTTP header name: ${name}`);
|
|
1562
|
+
if (typeof value !== "string") throw new Error(`HTTP header "${name}" must be a string`);
|
|
1563
|
+
assertNoLineBreak(value, `headers.${name}`);
|
|
1564
|
+
headers[name] = value;
|
|
1565
|
+
}
|
|
1566
|
+
if (input.addHostHeader !== false && input.host && !hasHeader(headers, "Host")) {
|
|
1567
|
+
assertNoLineBreak(input.host, "host");
|
|
1568
|
+
headers.Host = input.host;
|
|
1569
|
+
}
|
|
1570
|
+
const body = input.body ?? "";
|
|
1571
|
+
if (input.body !== void 0 && input.addContentLength !== false && !hasHeader(headers, "Content-Length")) {
|
|
1572
|
+
if (!hasHeader(headers, "Transfer-Encoding")) headers["Content-Length"] = String(Buffer.byteLength(body, "utf8"));
|
|
1573
|
+
}
|
|
1574
|
+
if (input.addConnectionClose !== false && !hasHeader(headers, "Connection")) headers.Connection = "close";
|
|
1575
|
+
const startLine = `${method} ${target} HTTP/${httpVersion}`;
|
|
1576
|
+
const requestText = `${startLine}\r\n${buildHeaderSection(headers)}\r\n${body}`;
|
|
1577
|
+
const requestBuffer = Buffer.from(requestText, "utf8");
|
|
1578
|
+
return {
|
|
1579
|
+
requestText,
|
|
1580
|
+
requestHex: requestBuffer.toString("hex"),
|
|
1581
|
+
requestBytes: requestBuffer.length,
|
|
1582
|
+
startLine,
|
|
1583
|
+
headers,
|
|
1584
|
+
bodyBytes: Buffer.byteLength(body, "utf8"),
|
|
1585
|
+
httpVersion
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function analyzeHttpResponse(rawResponse, requestMethod) {
|
|
1589
|
+
const headerBytes = findHeaderTerminator(rawResponse);
|
|
1590
|
+
if (headerBytes === null) return null;
|
|
1591
|
+
const headerLines = rawResponse.subarray(0, headerBytes).toString("latin1").replace(/\r?\n\r?\n$/, "").split(/\r?\n/).filter((line) => line.length > 0);
|
|
1592
|
+
if (headerLines.length === 0) throw new Error("HTTP response did not contain a status line");
|
|
1593
|
+
const statusLine = headerLines[0];
|
|
1594
|
+
const statusMatch = /^HTTP\/(\d+\.\d+)\s+(\d{3})(?:\s+(.*))?$/.exec(statusLine);
|
|
1595
|
+
if (!statusMatch) throw new Error(`Invalid HTTP status line: ${statusLine}`);
|
|
1596
|
+
const rawHeaders = [];
|
|
1597
|
+
const headers = {};
|
|
1598
|
+
for (const line of headerLines.slice(1)) {
|
|
1599
|
+
const separator = line.indexOf(":");
|
|
1600
|
+
if (separator <= 0) continue;
|
|
1601
|
+
const name = line.slice(0, separator).trim();
|
|
1602
|
+
const value = line.slice(separator + 1).trim();
|
|
1603
|
+
rawHeaders.push({
|
|
1604
|
+
name,
|
|
1605
|
+
value
|
|
1606
|
+
});
|
|
1607
|
+
if (!(name in headers)) headers[name] = value;
|
|
1608
|
+
else if (name.toLowerCase() === "set-cookie") headers[name] = `${headers[name]}, ${value}`;
|
|
1609
|
+
else headers[name] = `${headers[name]}, ${value}`;
|
|
1610
|
+
}
|
|
1611
|
+
const statusCode = Number.parseInt(statusMatch[2], 10);
|
|
1612
|
+
const responseBody = rawResponse.subarray(headerBytes);
|
|
1613
|
+
if (isBodylessResponse(statusCode, requestMethod)) return {
|
|
1614
|
+
statusLine,
|
|
1615
|
+
httpVersion: statusMatch[1],
|
|
1616
|
+
statusCode,
|
|
1617
|
+
statusText: statusMatch[3] ?? "",
|
|
1618
|
+
headers,
|
|
1619
|
+
rawHeaders,
|
|
1620
|
+
headerBytes,
|
|
1621
|
+
bodyBytes: 0,
|
|
1622
|
+
bodyBuffer: Buffer.alloc(0),
|
|
1623
|
+
bodyMode: "none",
|
|
1624
|
+
complete: true,
|
|
1625
|
+
expectedRawBytes: headerBytes,
|
|
1626
|
+
chunkedDecoded: false
|
|
1627
|
+
};
|
|
1628
|
+
if (findHeaderValue(rawHeaders, "transfer-encoding")?.toLowerCase().includes("chunked")) {
|
|
1629
|
+
const decoded = decodeChunkedBody(responseBody);
|
|
1630
|
+
return {
|
|
1631
|
+
statusLine,
|
|
1632
|
+
httpVersion: statusMatch[1],
|
|
1633
|
+
statusCode,
|
|
1634
|
+
statusText: statusMatch[3] ?? "",
|
|
1635
|
+
headers,
|
|
1636
|
+
rawHeaders,
|
|
1637
|
+
headerBytes,
|
|
1638
|
+
bodyBytes: decoded.body.length,
|
|
1639
|
+
bodyBuffer: decoded.complete ? decoded.body : responseBody,
|
|
1640
|
+
bodyMode: "chunked",
|
|
1641
|
+
complete: decoded.complete,
|
|
1642
|
+
expectedRawBytes: decoded.complete ? headerBytes + decoded.consumedBytes : null,
|
|
1643
|
+
chunkedDecoded: decoded.complete
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
const contentLengthValue = findHeaderValue(rawHeaders, "content-length");
|
|
1647
|
+
if (contentLengthValue !== null) {
|
|
1648
|
+
const contentLength = Number.parseInt(contentLengthValue, 10);
|
|
1649
|
+
if (Number.isFinite(contentLength) && contentLength >= 0) {
|
|
1650
|
+
const bodyBuffer = responseBody.subarray(0, Math.min(responseBody.length, contentLength));
|
|
1651
|
+
return {
|
|
1652
|
+
statusLine,
|
|
1653
|
+
httpVersion: statusMatch[1],
|
|
1654
|
+
statusCode,
|
|
1655
|
+
statusText: statusMatch[3] ?? "",
|
|
1656
|
+
headers,
|
|
1657
|
+
rawHeaders,
|
|
1658
|
+
headerBytes,
|
|
1659
|
+
bodyBytes: bodyBuffer.length,
|
|
1660
|
+
bodyBuffer,
|
|
1661
|
+
bodyMode: "content-length",
|
|
1662
|
+
complete: responseBody.length >= contentLength,
|
|
1663
|
+
expectedRawBytes: headerBytes + contentLength,
|
|
1664
|
+
chunkedDecoded: false
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return {
|
|
1669
|
+
statusLine,
|
|
1670
|
+
httpVersion: statusMatch[1],
|
|
1671
|
+
statusCode,
|
|
1672
|
+
statusText: statusMatch[3] ?? "",
|
|
1673
|
+
headers,
|
|
1674
|
+
rawHeaders,
|
|
1675
|
+
headerBytes,
|
|
1676
|
+
bodyBytes: responseBody.length,
|
|
1677
|
+
bodyBuffer: responseBody,
|
|
1678
|
+
bodyMode: "until-close",
|
|
1679
|
+
complete: false,
|
|
1680
|
+
expectedRawBytes: null,
|
|
1681
|
+
chunkedDecoded: false
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function isLikelyTextHttpBody(contentType, body) {
|
|
1685
|
+
if (body.length === 0) return true;
|
|
1686
|
+
if (contentType && TEXTUAL_CONTENT_TYPE_RE.test(contentType)) return true;
|
|
1687
|
+
const sample = body.subarray(0, Math.min(body.length, 64));
|
|
1688
|
+
for (const byte of sample) if (byte === 0) return false;
|
|
1689
|
+
return true;
|
|
1690
|
+
}
|
|
1691
|
+
//#endregion
|
|
1692
|
+
//#region src/server/domains/network/http2-raw.ts
|
|
1693
|
+
const HTTP2_MAX_FRAME_SIZE = 16777215;
|
|
1694
|
+
const HTTP2_MAX_STREAM_ID = 2147483647;
|
|
1695
|
+
const HTTP2_MAX_SETTINGS_ID = 65535;
|
|
1696
|
+
const HTTP2_MAX_UNSIGNED_INT32 = 4294967295;
|
|
1697
|
+
const FRAME_TYPE_CODES = {
|
|
1698
|
+
DATA: 0,
|
|
1699
|
+
RST_STREAM: 3,
|
|
1700
|
+
SETTINGS: 4,
|
|
1701
|
+
PING: 6,
|
|
1702
|
+
GOAWAY: 7,
|
|
1703
|
+
WINDOW_UPDATE: 8
|
|
1704
|
+
};
|
|
1705
|
+
function assertIntegerInRange(value, field, min, max) {
|
|
1706
|
+
if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${field} must be an integer between ${String(min)} and ${String(max)}`);
|
|
1707
|
+
}
|
|
1708
|
+
function parseHexBytes(value, field) {
|
|
1709
|
+
const normalized = value.replace(/\s+/g, "").trim();
|
|
1710
|
+
if (normalized.length === 0) return Buffer.alloc(0);
|
|
1711
|
+
if (normalized.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(normalized)) throw new Error(`${field} must be an even-length hexadecimal string`);
|
|
1712
|
+
return Buffer.from(normalized, "hex");
|
|
1713
|
+
}
|
|
1714
|
+
function encodeTextBytes(value, encoding) {
|
|
1715
|
+
return Buffer.from(value, encoding);
|
|
1716
|
+
}
|
|
1717
|
+
function resolvePayloadBytes(payloadHex, payloadText, payloadEncoding) {
|
|
1718
|
+
if (payloadHex !== void 0 && payloadText !== void 0) throw new Error("payloadHex and payloadText are mutually exclusive");
|
|
1719
|
+
if (payloadHex !== void 0) return parseHexBytes(payloadHex, "payloadHex");
|
|
1720
|
+
if (payloadText !== void 0) return encodeTextBytes(payloadText, payloadEncoding);
|
|
1721
|
+
return Buffer.alloc(0);
|
|
1722
|
+
}
|
|
1723
|
+
function buildSettingsPayload(entries) {
|
|
1724
|
+
const payload = Buffer.alloc(entries.length * 6);
|
|
1725
|
+
entries.forEach((entry, index) => {
|
|
1726
|
+
assertIntegerInRange(entry.id, `settings[${String(index)}].id`, 0, HTTP2_MAX_SETTINGS_ID);
|
|
1727
|
+
assertIntegerInRange(entry.value, `settings[${String(index)}].value`, 0, HTTP2_MAX_UNSIGNED_INT32);
|
|
1728
|
+
payload.writeUInt16BE(entry.id, index * 6);
|
|
1729
|
+
payload.writeUInt32BE(entry.value >>> 0, index * 6 + 2);
|
|
1730
|
+
});
|
|
1731
|
+
return payload;
|
|
1732
|
+
}
|
|
1733
|
+
function encodeUInt31(value, field) {
|
|
1734
|
+
assertIntegerInRange(value, field, 0, HTTP2_MAX_STREAM_ID);
|
|
1735
|
+
const buffer = Buffer.alloc(4);
|
|
1736
|
+
buffer.writeUInt32BE(value >>> 0, 0);
|
|
1737
|
+
buffer[0] = buffer[0] & 127;
|
|
1738
|
+
return buffer;
|
|
1739
|
+
}
|
|
1740
|
+
function buildFramePayload(input) {
|
|
1741
|
+
const frameType = input.frameType;
|
|
1742
|
+
const flags = input.flags ?? 0;
|
|
1743
|
+
assertIntegerInRange(flags, "flags", 0, 255);
|
|
1744
|
+
switch (frameType) {
|
|
1745
|
+
case "DATA": return {
|
|
1746
|
+
payload: resolvePayloadBytes(input.payloadHex, input.payloadText, input.payloadEncoding ?? "utf8"),
|
|
1747
|
+
typeCode: FRAME_TYPE_CODES.DATA,
|
|
1748
|
+
flags
|
|
1749
|
+
};
|
|
1750
|
+
case "SETTINGS":
|
|
1751
|
+
if (input.ack === true && (input.settings?.length ?? 0) > 0) throw new Error("SETTINGS ack frames must not include settings payload");
|
|
1752
|
+
return {
|
|
1753
|
+
payload: buildSettingsPayload(input.settings ?? []),
|
|
1754
|
+
typeCode: FRAME_TYPE_CODES.SETTINGS,
|
|
1755
|
+
flags: input.ack ? flags | 1 : flags
|
|
1756
|
+
};
|
|
1757
|
+
case "PING": {
|
|
1758
|
+
const payload = input.pingOpaqueDataHex ? parseHexBytes(input.pingOpaqueDataHex, "pingOpaqueDataHex") : Buffer.alloc(8);
|
|
1759
|
+
if (payload.length !== 8) throw new Error("PING frames require exactly 8 bytes of opaque data");
|
|
1760
|
+
return {
|
|
1761
|
+
payload,
|
|
1762
|
+
typeCode: FRAME_TYPE_CODES.PING,
|
|
1763
|
+
flags: input.ack ? flags | 1 : flags
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
case "WINDOW_UPDATE": {
|
|
1767
|
+
const increment = input.windowSizeIncrement;
|
|
1768
|
+
if (increment === void 0) throw new Error("windowSizeIncrement is required for WINDOW_UPDATE frames");
|
|
1769
|
+
assertIntegerInRange(increment, "windowSizeIncrement", 1, HTTP2_MAX_STREAM_ID);
|
|
1770
|
+
return {
|
|
1771
|
+
payload: encodeUInt31(increment, "windowSizeIncrement"),
|
|
1772
|
+
typeCode: FRAME_TYPE_CODES.WINDOW_UPDATE,
|
|
1773
|
+
flags
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
case "RST_STREAM": {
|
|
1777
|
+
const errorCode = input.errorCode ?? 0;
|
|
1778
|
+
assertIntegerInRange(errorCode, "errorCode", 0, HTTP2_MAX_UNSIGNED_INT32);
|
|
1779
|
+
const payload = Buffer.alloc(4);
|
|
1780
|
+
payload.writeUInt32BE(errorCode >>> 0, 0);
|
|
1781
|
+
return {
|
|
1782
|
+
payload,
|
|
1783
|
+
typeCode: FRAME_TYPE_CODES.RST_STREAM,
|
|
1784
|
+
flags
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
case "GOAWAY": {
|
|
1788
|
+
const lastStreamId = input.lastStreamId ?? 0;
|
|
1789
|
+
const errorCode = input.errorCode ?? 0;
|
|
1790
|
+
assertIntegerInRange(lastStreamId, "lastStreamId", 0, HTTP2_MAX_STREAM_ID);
|
|
1791
|
+
assertIntegerInRange(errorCode, "errorCode", 0, HTTP2_MAX_UNSIGNED_INT32);
|
|
1792
|
+
const debugData = input.debugDataText !== void 0 ? encodeTextBytes(input.debugDataText, input.debugDataEncoding ?? "utf8") : Buffer.alloc(0);
|
|
1793
|
+
return {
|
|
1794
|
+
payload: Buffer.concat([
|
|
1795
|
+
encodeUInt31(lastStreamId, "lastStreamId"),
|
|
1796
|
+
Buffer.from([
|
|
1797
|
+
errorCode >>> 24 & 255,
|
|
1798
|
+
errorCode >>> 16 & 255,
|
|
1799
|
+
errorCode >>> 8 & 255,
|
|
1800
|
+
errorCode & 255
|
|
1801
|
+
]),
|
|
1802
|
+
debugData
|
|
1803
|
+
]),
|
|
1804
|
+
typeCode: FRAME_TYPE_CODES.GOAWAY,
|
|
1805
|
+
flags
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
case "RAW": {
|
|
1809
|
+
const typeCode = input.frameTypeCode;
|
|
1810
|
+
if (typeCode === void 0) throw new Error("frameTypeCode is required when frameType is RAW");
|
|
1811
|
+
assertIntegerInRange(typeCode, "frameTypeCode", 0, 255);
|
|
1812
|
+
return {
|
|
1813
|
+
payload: resolvePayloadBytes(input.payloadHex, input.payloadText, input.payloadEncoding ?? "utf8"),
|
|
1814
|
+
typeCode,
|
|
1815
|
+
flags
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function validateFrameTypeStream(frameType, streamId) {
|
|
1821
|
+
if ((frameType === "SETTINGS" || frameType === "PING" || frameType === "GOAWAY") && streamId !== 0) throw new Error(`${frameType} frames must use streamId 0`);
|
|
1822
|
+
if ((frameType === "DATA" || frameType === "RST_STREAM") && streamId === 0) throw new Error(`${frameType} frames must use a non-zero streamId`);
|
|
1823
|
+
}
|
|
1824
|
+
function buildHttp2Frame(input) {
|
|
1825
|
+
const streamId = input.streamId ?? 0;
|
|
1826
|
+
assertIntegerInRange(streamId, "streamId", 0, HTTP2_MAX_STREAM_ID);
|
|
1827
|
+
validateFrameTypeStream(input.frameType, streamId);
|
|
1828
|
+
const { payload, typeCode, flags } = buildFramePayload(input);
|
|
1829
|
+
if (payload.length > HTTP2_MAX_FRAME_SIZE) throw new Error(`payload exceeds the HTTP/2 maximum frame size of ${String(HTTP2_MAX_FRAME_SIZE)} bytes`);
|
|
1830
|
+
const header = Buffer.alloc(9);
|
|
1831
|
+
header[0] = payload.length >>> 16 & 255;
|
|
1832
|
+
header[1] = payload.length >>> 8 & 255;
|
|
1833
|
+
header[2] = payload.length & 255;
|
|
1834
|
+
header[3] = typeCode & 255;
|
|
1835
|
+
header[4] = flags & 255;
|
|
1836
|
+
header.writeUInt32BE(streamId >>> 0, 5);
|
|
1837
|
+
header[5] = header[5] & 127;
|
|
1838
|
+
const frame = Buffer.concat([header, payload]);
|
|
1839
|
+
return {
|
|
1840
|
+
frameType: input.frameType,
|
|
1841
|
+
typeCode,
|
|
1842
|
+
streamId,
|
|
1843
|
+
flags,
|
|
1844
|
+
payloadBytes: payload.length,
|
|
1845
|
+
payloadHex: payload.toString("hex"),
|
|
1846
|
+
frameHeaderHex: header.toString("hex"),
|
|
1847
|
+
frameHex: frame.toString("hex")
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
//#endregion
|
|
1851
|
+
//#region src/server/domains/network/handlers/raw-helpers.ts
|
|
1852
|
+
const HTTP_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
1853
|
+
function parseOptionalString(value, field) {
|
|
1854
|
+
if (value === void 0) return void 0;
|
|
1855
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
1856
|
+
const trimmed = value.trim();
|
|
1857
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1858
|
+
}
|
|
1859
|
+
function parseRawString(value, field, options = {}) {
|
|
1860
|
+
if (value === void 0) return void 0;
|
|
1861
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
1862
|
+
if (value.length === 0 && !options.allowEmpty) return void 0;
|
|
1863
|
+
return value;
|
|
1864
|
+
}
|
|
1865
|
+
function parseOptionalBoolean(value, field) {
|
|
1866
|
+
if (value === void 0) return void 0;
|
|
1867
|
+
if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
|
|
1868
|
+
return value;
|
|
1869
|
+
}
|
|
1870
|
+
function parseStringArray(value, field) {
|
|
1871
|
+
if (value === void 0) return [];
|
|
1872
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${field} must be an array of strings`);
|
|
1873
|
+
return value.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1874
|
+
}
|
|
1875
|
+
function parseHeaderRecord(value, field) {
|
|
1876
|
+
if (value === void 0) return void 0;
|
|
1877
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${field} must be an object`);
|
|
1878
|
+
const headers = {};
|
|
1879
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
1880
|
+
if (!HTTP_TOKEN_RE.test(name)) throw new Error(`${field} contains an invalid HTTP header name: ${name}`);
|
|
1881
|
+
if (typeof headerValue !== "string") throw new Error(`${field}.${name} must be a string`);
|
|
1882
|
+
headers[name] = headerValue;
|
|
1883
|
+
}
|
|
1884
|
+
return headers;
|
|
1885
|
+
}
|
|
1886
|
+
function parseNetworkAuthorization(value, field = "authorization") {
|
|
1887
|
+
if (value === void 0) return void 0;
|
|
1888
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${field} must be an object`);
|
|
1889
|
+
const record = value;
|
|
1890
|
+
const allowedHosts = parseStringArray(record.allowedHosts, `${field}.allowedHosts`);
|
|
1891
|
+
const allowedCidrs = parseStringArray(record.allowedCidrs, `${field}.allowedCidrs`);
|
|
1892
|
+
const allowPrivateNetwork = parseOptionalBoolean(record.allowPrivateNetwork, `${field}.allowPrivateNetwork`);
|
|
1893
|
+
const allowInsecureHttp = parseOptionalBoolean(record.allowInsecureHttp, `${field}.allowInsecureHttp`);
|
|
1894
|
+
const expiresAt = parseOptionalString(record.expiresAt, `${field}.expiresAt`);
|
|
1895
|
+
const reason = parseOptionalString(record.reason, `${field}.reason`);
|
|
1896
|
+
const authorization = {};
|
|
1897
|
+
if (allowedHosts.length > 0) authorization.allowedHosts = allowedHosts;
|
|
1898
|
+
if (allowedCidrs.length > 0) authorization.allowedCidrs = allowedCidrs;
|
|
1899
|
+
if (allowPrivateNetwork !== void 0) authorization.allowPrivateNetwork = allowPrivateNetwork;
|
|
1900
|
+
if (allowInsecureHttp !== void 0) authorization.allowInsecureHttp = allowInsecureHttp;
|
|
1901
|
+
if (expiresAt !== void 0) authorization.expiresAt = expiresAt;
|
|
1902
|
+
if (reason !== void 0) authorization.reason = reason;
|
|
1903
|
+
return authorization;
|
|
1904
|
+
}
|
|
1905
|
+
function clamp(value, min, max) {
|
|
1906
|
+
return Math.min(Math.max(value, min), max);
|
|
1907
|
+
}
|
|
1908
|
+
function roundMs(ms) {
|
|
1909
|
+
return Math.round(ms * 100) / 100;
|
|
1910
|
+
}
|
|
1911
|
+
function computeRttStats(samples) {
|
|
1912
|
+
const sorted = [...samples].toSorted((a, b) => a - b);
|
|
1913
|
+
if (sorted.length === 0) return null;
|
|
1914
|
+
return {
|
|
1915
|
+
count: sorted.length,
|
|
1916
|
+
minMs: sorted[0],
|
|
1917
|
+
maxMs: sorted[sorted.length - 1],
|
|
1918
|
+
avgMs: roundMs(sorted.reduce((s, v) => s + v, 0) / sorted.length),
|
|
1919
|
+
p50Ms: sorted[Math.floor(sorted.length * .5)],
|
|
1920
|
+
p95Ms: sorted[Math.floor(sorted.length * .95)]
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
async function resolveAuthorizedTransportTarget(rawUrl, authorization, operationLabel) {
|
|
1924
|
+
let url;
|
|
1925
|
+
try {
|
|
1926
|
+
url = new URL(rawUrl);
|
|
1927
|
+
} catch {
|
|
1928
|
+
throw new Error("url must be an absolute http:// or https:// URL");
|
|
1929
|
+
}
|
|
1930
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("url must use the http:// or https:// scheme");
|
|
1931
|
+
const authorizationPolicy = createNetworkAuthorizationPolicy(authorization);
|
|
1932
|
+
const allowLegacyLocalSsrf = !authorizationPolicy && isLocalSsrfBypassEnabled();
|
|
1933
|
+
if (authorizationPolicy && (authorizationPolicy.allowPrivateNetwork || authorizationPolicy.allowInsecureHttp) && !hasAuthorizedTargets(authorizationPolicy)) throw new Error("authorization must include at least one allowed host or CIDR when enabling private network or insecure HTTP access.");
|
|
1934
|
+
if (isNetworkAuthorizationExpired(authorizationPolicy)) throw new Error("authorization expired before the request was executed.");
|
|
1935
|
+
let target;
|
|
1936
|
+
try {
|
|
1937
|
+
target = await resolveNetworkTarget(url.toString());
|
|
1938
|
+
} catch {
|
|
1939
|
+
throw new Error(`${operationLabel} blocked: DNS resolution failed for "${url.toString()}"`);
|
|
1940
|
+
}
|
|
1941
|
+
const isPrivateTargetAllowed = (resolvedTarget) => {
|
|
1942
|
+
if (allowLegacyLocalSsrf) return true;
|
|
1943
|
+
return authorizationPolicy?.allowPrivateNetwork === true && isAuthorizedNetworkTarget(authorizationPolicy, resolvedTarget);
|
|
1944
|
+
};
|
|
1945
|
+
const isInsecureHttpAllowed = (resolvedTarget) => {
|
|
1946
|
+
if (allowLegacyLocalSsrf) return true;
|
|
1947
|
+
if (isLoopbackHost(resolvedTarget.hostname)) return true;
|
|
1948
|
+
return authorizationPolicy?.allowInsecureHttp === true && isAuthorizedNetworkTarget(authorizationPolicy, resolvedTarget);
|
|
1949
|
+
};
|
|
1950
|
+
const effectivePort = Number.parseInt(url.port || (url.protocol === "https:" ? "443" : "80"), 10);
|
|
1951
|
+
if (url.protocol === "http:" && !isInsecureHttpAllowed(target)) throw new Error(`${operationLabel} blocked: insecure HTTP is only allowed for loopback or explicitly authorized targets, got "${target.hostname}:${String(effectivePort)}"`);
|
|
1952
|
+
const hostnameIsPrivate = isPrivateHost(target.hostname);
|
|
1953
|
+
const resolvedAddressIsPrivate = isPrivateHost(target.resolvedAddress ?? "");
|
|
1954
|
+
const loopbackTarget = isLoopbackHost(target.hostname) || isLoopbackHost(target.resolvedAddress ?? "");
|
|
1955
|
+
if ((hostnameIsPrivate || resolvedAddressIsPrivate) && !loopbackTarget && !isPrivateTargetAllowed(target)) {
|
|
1956
|
+
if (!hostnameIsPrivate && resolvedAddressIsPrivate && target.resolvedAddress) throw new Error(`${operationLabel} blocked: "${target.hostname}:${String(effectivePort)}" resolved to private IP ${target.resolvedAddress}`);
|
|
1957
|
+
throw new Error(`${operationLabel} blocked: target "${target.hostname}:${String(effectivePort)}" resolves to a private or reserved address.`);
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
url,
|
|
1961
|
+
target,
|
|
1962
|
+
authorizationPolicy,
|
|
1963
|
+
allowLegacyLocalSsrf
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
function normalizeTargetHost(host) {
|
|
1967
|
+
const trimmed = host.trim();
|
|
1968
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1);
|
|
1969
|
+
return trimmed;
|
|
1970
|
+
}
|
|
1971
|
+
function formatHostForUrl(host) {
|
|
1972
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1973
|
+
}
|
|
1974
|
+
function getRequestMethod(requestText) {
|
|
1975
|
+
const method = (requestText.split(/\r?\n/, 1)[0]?.trim() ?? "").split(/\s+/, 1)[0]?.trim().toUpperCase() ?? "";
|
|
1976
|
+
if (!HTTP_TOKEN_RE.test(method)) throw new Error("requestText must start with a valid HTTP request line");
|
|
1977
|
+
return method;
|
|
1978
|
+
}
|
|
1979
|
+
async function exchangePlainHttp(host, port, requestBuffer, requestMethod, timeoutMs, maxResponseBytes) {
|
|
1980
|
+
return await new Promise((resolve, reject) => {
|
|
1981
|
+
const socket = net.createConnection({
|
|
1982
|
+
host,
|
|
1983
|
+
port
|
|
1984
|
+
});
|
|
1985
|
+
let settled = false;
|
|
1986
|
+
let sawData = false;
|
|
1987
|
+
const responseChain = new BufferChain();
|
|
1988
|
+
const cleanup = () => {
|
|
1989
|
+
socket.removeAllListeners();
|
|
1990
|
+
socket.destroy();
|
|
1991
|
+
};
|
|
1992
|
+
const finalize = (endedBy) => {
|
|
1993
|
+
if (settled) return;
|
|
1994
|
+
settled = true;
|
|
1995
|
+
cleanup();
|
|
1996
|
+
resolve({
|
|
1997
|
+
rawResponse: responseChain.toBuffer(),
|
|
1998
|
+
endedBy
|
|
1999
|
+
});
|
|
2000
|
+
};
|
|
2001
|
+
const fail = (error) => {
|
|
2002
|
+
if (settled) return;
|
|
2003
|
+
settled = true;
|
|
2004
|
+
cleanup();
|
|
2005
|
+
reject(error);
|
|
2006
|
+
};
|
|
2007
|
+
socket.setTimeout(timeoutMs);
|
|
2008
|
+
socket.once("connect", () => {
|
|
2009
|
+
socket.end(requestBuffer);
|
|
2010
|
+
});
|
|
2011
|
+
socket.on("data", (chunk) => {
|
|
2012
|
+
sawData = true;
|
|
2013
|
+
responseChain.append(chunk);
|
|
2014
|
+
if (responseChain.length > maxResponseBytes) {
|
|
2015
|
+
finalize("max-bytes");
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const analysis = analyzeHttpResponse(responseChain.toBuffer(), requestMethod);
|
|
2019
|
+
if (!analysis || !analysis.complete) return;
|
|
2020
|
+
if (analysis.bodyMode === "none") {
|
|
2021
|
+
finalize("no-body");
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
if (analysis.bodyMode === "content-length") {
|
|
2025
|
+
finalize("content-length");
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (analysis.bodyMode === "chunked") finalize("chunked");
|
|
2029
|
+
});
|
|
2030
|
+
socket.once("timeout", () => {
|
|
2031
|
+
if (!sawData) {
|
|
2032
|
+
fail(/* @__PURE__ */ new Error(`Timed out waiting for HTTP response from ${host}:${String(port)}`));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
finalize("timeout");
|
|
2036
|
+
});
|
|
2037
|
+
socket.once("end", () => {
|
|
2038
|
+
finalize("socket-close");
|
|
2039
|
+
});
|
|
2040
|
+
socket.once("error", (error) => {
|
|
2041
|
+
fail(error);
|
|
2042
|
+
});
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
function normalizeHttp2HeaderValue(value) {
|
|
2046
|
+
if (value === void 0) return null;
|
|
2047
|
+
if (Array.isArray(value)) return value.map((entry) => String(entry));
|
|
2048
|
+
return String(value);
|
|
2049
|
+
}
|
|
2050
|
+
function normalizeHttp2Headers(headers) {
|
|
2051
|
+
const normalized = {};
|
|
2052
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2053
|
+
const normalizedValue = normalizeHttp2HeaderValue(value);
|
|
2054
|
+
if (normalizedValue !== null) normalized[name] = normalizedValue;
|
|
2055
|
+
}
|
|
2056
|
+
return normalized;
|
|
2057
|
+
}
|
|
2058
|
+
function normalizeAlpnProtocol(protocol) {
|
|
2059
|
+
if (!protocol) return null;
|
|
2060
|
+
const trimmed = protocol.trim();
|
|
2061
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2062
|
+
}
|
|
2063
|
+
function toHttp2RequestHeaders(headers) {
|
|
2064
|
+
const output = {};
|
|
2065
|
+
for (const [name, value] of Object.entries(headers ?? {})) output[name.toLowerCase()] = value;
|
|
2066
|
+
return output;
|
|
2067
|
+
}
|
|
2068
|
+
function performHttp2ProbeInternal(options) {
|
|
2069
|
+
const { url, target, method, requestHeaders, bodyBuffer, timeoutMs, maxBodyBytes, effectivePort, requestedAlpnProtocols } = options;
|
|
2070
|
+
let observedAlpnProtocol = null;
|
|
2071
|
+
return new Promise((resolve, reject) => {
|
|
2072
|
+
let settled = false;
|
|
2073
|
+
let responseHeaders;
|
|
2074
|
+
const bodyChain = new BufferChain();
|
|
2075
|
+
let truncated = false;
|
|
2076
|
+
let request = null;
|
|
2077
|
+
let connectedSocket = null;
|
|
2078
|
+
const session = http2.connect(url.origin, { createConnection: () => {
|
|
2079
|
+
if (url.protocol === "https:") {
|
|
2080
|
+
const socket = tls.connect({
|
|
2081
|
+
host: target.resolvedAddress ?? target.hostname,
|
|
2082
|
+
port: effectivePort,
|
|
2083
|
+
servername: target.hostname,
|
|
2084
|
+
ALPNProtocols: requestedAlpnProtocols,
|
|
2085
|
+
rejectUnauthorized: true
|
|
2086
|
+
});
|
|
2087
|
+
socket.setTimeout(timeoutMs, () => {
|
|
2088
|
+
socket.destroy(/* @__PURE__ */ new Error(`Timed out probing HTTP/2 endpoint ${url.toString()}`));
|
|
2089
|
+
});
|
|
2090
|
+
socket.once("secureConnect", () => {
|
|
2091
|
+
observedAlpnProtocol = normalizeAlpnProtocol(socket.alpnProtocol);
|
|
2092
|
+
});
|
|
2093
|
+
connectedSocket = socket;
|
|
2094
|
+
return socket;
|
|
2095
|
+
}
|
|
2096
|
+
const socket = net.connect({
|
|
2097
|
+
host: target.resolvedAddress ?? target.hostname,
|
|
2098
|
+
port: effectivePort
|
|
2099
|
+
});
|
|
2100
|
+
socket.setTimeout(timeoutMs, () => {
|
|
2101
|
+
socket.destroy(/* @__PURE__ */ new Error(`Timed out probing HTTP/2 endpoint ${url.toString()}`));
|
|
2102
|
+
});
|
|
2103
|
+
connectedSocket = socket;
|
|
2104
|
+
return socket;
|
|
2105
|
+
} });
|
|
2106
|
+
const cleanup = () => {
|
|
2107
|
+
request?.removeAllListeners();
|
|
2108
|
+
session.removeAllListeners();
|
|
2109
|
+
};
|
|
2110
|
+
const finish = () => {
|
|
2111
|
+
if (settled) return;
|
|
2112
|
+
settled = true;
|
|
2113
|
+
cleanup();
|
|
2114
|
+
session.close();
|
|
2115
|
+
resolve({
|
|
2116
|
+
responseHeaders: responseHeaders ?? {},
|
|
2117
|
+
bodyBuffer: bodyChain.toBuffer(),
|
|
2118
|
+
truncated,
|
|
2119
|
+
alpnProtocol: observedAlpnProtocol
|
|
2120
|
+
});
|
|
2121
|
+
};
|
|
2122
|
+
const fail = (error) => {
|
|
2123
|
+
if (settled) return;
|
|
2124
|
+
settled = true;
|
|
2125
|
+
cleanup();
|
|
2126
|
+
session.destroy(error);
|
|
2127
|
+
reject(error);
|
|
2128
|
+
};
|
|
2129
|
+
session.once("error", (error) => {
|
|
2130
|
+
if (connectedSocket instanceof tls.TLSSocket) observedAlpnProtocol = normalizeAlpnProtocol(connectedSocket.alpnProtocol);
|
|
2131
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
2132
|
+
});
|
|
2133
|
+
session.once("connect", () => {
|
|
2134
|
+
if (connectedSocket instanceof tls.TLSSocket) observedAlpnProtocol = normalizeAlpnProtocol(connectedSocket.alpnProtocol);
|
|
2135
|
+
request = session.request({
|
|
2136
|
+
":method": method,
|
|
2137
|
+
":path": `${url.pathname}${url.search}`,
|
|
2138
|
+
":scheme": url.protocol.slice(0, -1),
|
|
2139
|
+
":authority": url.host,
|
|
2140
|
+
...requestHeaders
|
|
2141
|
+
});
|
|
2142
|
+
request.once("response", (headers) => {
|
|
2143
|
+
responseHeaders = headers;
|
|
2144
|
+
});
|
|
2145
|
+
request.on("data", (chunk) => {
|
|
2146
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
|
|
2147
|
+
const remaining = maxBodyBytes - bodyChain.length;
|
|
2148
|
+
if (remaining > 0) bodyChain.append(buffer.subarray(0, remaining));
|
|
2149
|
+
if (buffer.length > remaining && !truncated) {
|
|
2150
|
+
truncated = true;
|
|
2151
|
+
request?.close(http2.constants.NGHTTP2_CANCEL);
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
request.once("end", finish);
|
|
2155
|
+
request.once("close", () => {
|
|
2156
|
+
if (truncated) finish();
|
|
2157
|
+
});
|
|
2158
|
+
request.once("error", (error) => {
|
|
2159
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
2160
|
+
});
|
|
2161
|
+
if (bodyBuffer.length > 0) request.end(bodyBuffer);
|
|
2162
|
+
else request.end();
|
|
2163
|
+
});
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
//#endregion
|
|
2167
|
+
//#region src/native/IcmpProbe.ts
|
|
2168
|
+
/**
|
|
2169
|
+
* Cross-platform ICMP probe and traceroute via koffi FFI.
|
|
2170
|
+
*
|
|
2171
|
+
* Windows: IcmpSendEcho from iphlpapi.dll (no admin required).
|
|
2172
|
+
* Linux/macOS: Raw ICMP sockets via libc (requires root/CAP_NET_RAW).
|
|
2173
|
+
*
|
|
2174
|
+
* Uses Buffer-based struct parsing (same pattern as Win32API.ts)
|
|
2175
|
+
* to avoid koffi struct registration issues in test environments.
|
|
2176
|
+
*/
|
|
2177
|
+
function ipToString(addr) {
|
|
2178
|
+
return `${addr & 255}.${addr >>> 8 & 255}.${addr >>> 16 & 255}.${addr >>> 24 & 255}`;
|
|
2179
|
+
}
|
|
2180
|
+
function isValidIpv4(ip) {
|
|
2181
|
+
const parts = ip.split(".");
|
|
2182
|
+
if (parts.length !== 4) return false;
|
|
2183
|
+
return parts.every((p) => {
|
|
2184
|
+
const n = parseInt(p, 10);
|
|
2185
|
+
return !isNaN(n) && n >= 0 && n <= 255 && p === String(n);
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
let _available = null;
|
|
2189
|
+
const IP_STATUS = {
|
|
2190
|
+
0: "SUCCESS",
|
|
2191
|
+
11001: "BUF_TOO_SMALL",
|
|
2192
|
+
11002: "DEST_NET_UNREACHABLE",
|
|
2193
|
+
11003: "DEST_HOST_UNREACHABLE",
|
|
2194
|
+
11004: "DEST_PROT_UNREACHABLE",
|
|
2195
|
+
11005: "DEST_PORT_UNREACHABLE",
|
|
2196
|
+
11009: "PACKET_TOO_BIG",
|
|
2197
|
+
11010: "REQ_TIMED_OUT",
|
|
2198
|
+
11013: "TTL_EXPIRED_TRANSIT",
|
|
2199
|
+
11014: "TTL_EXPIRED_REASSEM",
|
|
2200
|
+
11015: "PARAM_PROBLEM",
|
|
2201
|
+
11016: "SOURCE_QUENCH",
|
|
2202
|
+
11050: "GENERAL_FAILURE"
|
|
2203
|
+
};
|
|
2204
|
+
function winStatusLabel(s) {
|
|
2205
|
+
return IP_STATUS[s] ?? `UNKNOWN_${s}`;
|
|
2206
|
+
}
|
|
2207
|
+
function winStatusClass(s) {
|
|
2208
|
+
if (s === 0) return "success";
|
|
2209
|
+
if (s === 11010) return "timeout";
|
|
2210
|
+
if (s === 11013 || s === 11014) return "time_exceeded";
|
|
2211
|
+
if (s >= 11002 && s <= 11005) return "destination_unreachable";
|
|
2212
|
+
if (s === 11016) return "source_quench";
|
|
2213
|
+
if (s === 11009) return "packet_too_big";
|
|
2214
|
+
if (s === 11015) return "parameter_problem";
|
|
2215
|
+
return "error";
|
|
2216
|
+
}
|
|
2217
|
+
let iphlpapi = null;
|
|
2218
|
+
let ws2_32 = null;
|
|
2219
|
+
function getIphlpapi() {
|
|
2220
|
+
if (!iphlpapi) {
|
|
2221
|
+
iphlpapi = koffi.load("iphlpapi.dll");
|
|
2222
|
+
logger.debug("Loaded iphlpapi.dll via koffi");
|
|
2223
|
+
}
|
|
2224
|
+
return iphlpapi;
|
|
2225
|
+
}
|
|
2226
|
+
function getWs2_32() {
|
|
2227
|
+
if (!ws2_32) {
|
|
2228
|
+
ws2_32 = koffi.load("ws2_32.dll");
|
|
2229
|
+
logger.debug("Loaded ws2_32.dll via koffi");
|
|
2230
|
+
}
|
|
2231
|
+
return ws2_32;
|
|
2232
|
+
}
|
|
2233
|
+
const IP_OPT_SIZE = 16;
|
|
2234
|
+
const MIN_REPLY_BUF_SIZE = 256;
|
|
2235
|
+
const ICMP_REPLY_OVERHEAD = 64;
|
|
2236
|
+
function getReplyBufferSize(packetSize) {
|
|
2237
|
+
return Math.max(MIN_REPLY_BUF_SIZE, packetSize + ICMP_REPLY_OVERHEAD);
|
|
2238
|
+
}
|
|
2239
|
+
function buildOptionBuf(ttl) {
|
|
2240
|
+
const buf = Buffer.alloc(IP_OPT_SIZE, 0);
|
|
2241
|
+
buf.writeUInt8(ttl, 0);
|
|
2242
|
+
return buf;
|
|
2243
|
+
}
|
|
2244
|
+
function parseReply(buf) {
|
|
2245
|
+
return {
|
|
2246
|
+
address: buf.readUInt32LE(0),
|
|
2247
|
+
status: buf.readUInt32LE(4),
|
|
2248
|
+
rtt: buf.readUInt32LE(8)
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function win_inet_addr(ip) {
|
|
2252
|
+
return getWs2_32().func("uint32 inet_addr(char *)")(ip);
|
|
2253
|
+
}
|
|
2254
|
+
function win_IcmpCreateFile() {
|
|
2255
|
+
return getIphlpapi().func("void * IcmpCreateFile()")();
|
|
2256
|
+
}
|
|
2257
|
+
function win_IcmpCloseHandle(h) {
|
|
2258
|
+
return getIphlpapi().func("int IcmpCloseHandle(void *)")(h) !== 0;
|
|
2259
|
+
}
|
|
2260
|
+
function win_IcmpSendEcho(handle, destAddr, sendData, optionBuf, timeoutMs) {
|
|
2261
|
+
const fn = getIphlpapi().func("uint32 IcmpSendEcho(void *, uint32, void *, uint16, void *, void *, uint32, uint32)");
|
|
2262
|
+
const replyBuf = Buffer.alloc(getReplyBufferSize(sendData.length));
|
|
2263
|
+
const n = fn(handle, destAddr, sendData, sendData.length, optionBuf, replyBuf, replyBuf.length, timeoutMs);
|
|
2264
|
+
return {
|
|
2265
|
+
numReplies: Number(n),
|
|
2266
|
+
replyBuf
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
function winIcmpProbe(params) {
|
|
2270
|
+
const { target, ttl = 128, packetSize = ICMP_DEFAULT_PACKET_SIZE, timeout = ICMP_PROBE_TIMEOUT_MS } = params;
|
|
2271
|
+
const destAddr = win_inet_addr(target);
|
|
2272
|
+
if (destAddr === 4294967295) return {
|
|
2273
|
+
target,
|
|
2274
|
+
ip: "",
|
|
2275
|
+
alive: false,
|
|
2276
|
+
rtt: null,
|
|
2277
|
+
ttl,
|
|
2278
|
+
icmpStatus: "INVALID_ADDRESS",
|
|
2279
|
+
errorClass: "error",
|
|
2280
|
+
packetSize
|
|
2281
|
+
};
|
|
2282
|
+
const handle = win_IcmpCreateFile();
|
|
2283
|
+
try {
|
|
2284
|
+
const { numReplies, replyBuf } = win_IcmpSendEcho(handle, destAddr, Buffer.alloc(packetSize, 170), buildOptionBuf(ttl), timeout);
|
|
2285
|
+
if (numReplies === 0) return {
|
|
2286
|
+
target,
|
|
2287
|
+
ip: ipToString(destAddr),
|
|
2288
|
+
alive: false,
|
|
2289
|
+
rtt: null,
|
|
2290
|
+
ttl,
|
|
2291
|
+
icmpStatus: "REQ_TIMED_OUT",
|
|
2292
|
+
errorClass: "timeout",
|
|
2293
|
+
packetSize
|
|
2294
|
+
};
|
|
2295
|
+
const reply = parseReply(replyBuf);
|
|
2296
|
+
return {
|
|
2297
|
+
target,
|
|
2298
|
+
ip: ipToString(reply.address),
|
|
2299
|
+
alive: reply.status === 0,
|
|
2300
|
+
rtt: reply.status === 0 ? reply.rtt : null,
|
|
2301
|
+
ttl,
|
|
2302
|
+
icmpStatus: winStatusLabel(reply.status),
|
|
2303
|
+
errorClass: winStatusClass(reply.status),
|
|
2304
|
+
packetSize
|
|
2305
|
+
};
|
|
2306
|
+
} finally {
|
|
2307
|
+
win_IcmpCloseHandle(handle);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function winTraceroute(params) {
|
|
2311
|
+
const { target, maxHops = ICMP_TRACEROUTE_MAX_HOPS, timeout = ICMP_PROBE_TIMEOUT_MS, packetSize = ICMP_DEFAULT_PACKET_SIZE } = params;
|
|
2312
|
+
const destAddr = win_inet_addr(target);
|
|
2313
|
+
if (destAddr === 4294967295) return {
|
|
2314
|
+
target,
|
|
2315
|
+
ip: "",
|
|
2316
|
+
hops: [],
|
|
2317
|
+
reached: false,
|
|
2318
|
+
totalHops: 0,
|
|
2319
|
+
totalTime: 0
|
|
2320
|
+
};
|
|
2321
|
+
const handle = win_IcmpCreateFile();
|
|
2322
|
+
const hops = [];
|
|
2323
|
+
const t0 = performance.now();
|
|
2324
|
+
try {
|
|
2325
|
+
for (let ttl = 1; ttl <= maxHops; ttl++) {
|
|
2326
|
+
const { numReplies, replyBuf } = win_IcmpSendEcho(handle, destAddr, Buffer.alloc(packetSize, 170), buildOptionBuf(ttl), timeout);
|
|
2327
|
+
if (numReplies === 0) {
|
|
2328
|
+
hops.push({
|
|
2329
|
+
hop: ttl,
|
|
2330
|
+
ip: null,
|
|
2331
|
+
rtt: null,
|
|
2332
|
+
status: "REQ_TIMED_OUT",
|
|
2333
|
+
errorClass: "timeout"
|
|
2334
|
+
});
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
const reply = parseReply(replyBuf);
|
|
2338
|
+
const hopIp = ipToString(reply.address);
|
|
2339
|
+
hops.push({
|
|
2340
|
+
hop: ttl,
|
|
2341
|
+
ip: hopIp,
|
|
2342
|
+
rtt: reply.rtt,
|
|
2343
|
+
status: winStatusLabel(reply.status),
|
|
2344
|
+
errorClass: winStatusClass(reply.status)
|
|
2345
|
+
});
|
|
2346
|
+
if (reply.status === 0) break;
|
|
2347
|
+
}
|
|
2348
|
+
} finally {
|
|
2349
|
+
win_IcmpCloseHandle(handle);
|
|
2350
|
+
}
|
|
2351
|
+
const last = hops[hops.length - 1];
|
|
2352
|
+
return {
|
|
2353
|
+
target,
|
|
2354
|
+
ip: ipToString(destAddr),
|
|
2355
|
+
hops,
|
|
2356
|
+
reached: last?.status === "SUCCESS",
|
|
2357
|
+
totalHops: hops.length,
|
|
2358
|
+
totalTime: Math.round((performance.now() - t0) * 100) / 100
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
const AF_INET = 2;
|
|
2362
|
+
const SOCK_RAW = 3;
|
|
2363
|
+
const IPPROTO_ICMP = 1;
|
|
2364
|
+
const IPPROTO_IP = 0;
|
|
2365
|
+
const IP_TTL = 2;
|
|
2366
|
+
const SOL_SOCKET = 1;
|
|
2367
|
+
const SO_RCVTIMEO = process.platform === "darwin" ? 4102 : 20;
|
|
2368
|
+
const POSIX_LIB = process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6";
|
|
2369
|
+
let posixLib = null;
|
|
2370
|
+
function getPosixLib() {
|
|
2371
|
+
if (!posixLib) {
|
|
2372
|
+
posixLib = koffi.load(POSIX_LIB);
|
|
2373
|
+
logger.debug(`Loaded ${POSIX_LIB} via koffi for ICMP`);
|
|
2374
|
+
}
|
|
2375
|
+
return posixLib;
|
|
2376
|
+
}
|
|
2377
|
+
function posixSocket(domain, type, protocol) {
|
|
2378
|
+
return getPosixLib().func("int socket(int, int, int)")(domain, type, protocol);
|
|
2379
|
+
}
|
|
2380
|
+
function posixSetsockopt(fd, level, optname, optval, optlen) {
|
|
2381
|
+
return getPosixLib().func("int setsockopt(int, int, int, void *, int)")(fd, level, optname, optval, optlen);
|
|
2382
|
+
}
|
|
2383
|
+
function posixSendto(fd, buf, addr) {
|
|
2384
|
+
return getPosixLib().func("int sendto(int, void *, int, int, void *, int)")(fd, buf, buf.length, 0, addr, 16);
|
|
2385
|
+
}
|
|
2386
|
+
function posixRecv(fd, buf) {
|
|
2387
|
+
return getPosixLib().func("int recv(int, void *, int, int)")(fd, buf, buf.length, 0);
|
|
2388
|
+
}
|
|
2389
|
+
function posixClose(fd) {
|
|
2390
|
+
return getPosixLib().func("int close(int)")(fd);
|
|
2391
|
+
}
|
|
2392
|
+
function computeChecksum(buf) {
|
|
2393
|
+
let sum = 0;
|
|
2394
|
+
for (let i = 0; i < buf.length - 1; i += 2) sum += buf.readUInt16BE(i);
|
|
2395
|
+
if (buf.length & 1) sum += (buf[buf.length - 1] ?? 0) << 8;
|
|
2396
|
+
while (sum > 65535) sum = (sum & 65535) + (sum >>> 16);
|
|
2397
|
+
return ~sum & 65535;
|
|
2398
|
+
}
|
|
2399
|
+
function buildIcmpEcho(id, seq, payloadSize) {
|
|
2400
|
+
const buf = Buffer.alloc(8 + payloadSize);
|
|
2401
|
+
buf[0] = 8;
|
|
2402
|
+
buf[1] = 0;
|
|
2403
|
+
buf.writeUInt16BE(id & 65535, 4);
|
|
2404
|
+
buf.writeUInt16BE(seq & 65535, 6);
|
|
2405
|
+
for (let i = 8; i < buf.length; i++) buf[i] = 170;
|
|
2406
|
+
buf.writeUInt16BE(computeChecksum(buf), 2);
|
|
2407
|
+
return buf;
|
|
2408
|
+
}
|
|
2409
|
+
function buildSockaddrIn(ip) {
|
|
2410
|
+
const buf = Buffer.alloc(16, 0);
|
|
2411
|
+
buf.writeUInt16LE(AF_INET, 0);
|
|
2412
|
+
const parts = ip.split(".").map(Number);
|
|
2413
|
+
buf[4] = parts[0] ?? 0;
|
|
2414
|
+
buf[5] = parts[1] ?? 0;
|
|
2415
|
+
buf[6] = parts[2] ?? 0;
|
|
2416
|
+
buf[7] = parts[3] ?? 0;
|
|
2417
|
+
return buf;
|
|
2418
|
+
}
|
|
2419
|
+
function parseIcmpPacket(buf, n, expectedId) {
|
|
2420
|
+
if (n < 20) return null;
|
|
2421
|
+
const ihl = ((buf[0] ?? 0) & 15) * 4;
|
|
2422
|
+
if (n < ihl + 8) return null;
|
|
2423
|
+
const icmpType = buf[ihl] ?? 0;
|
|
2424
|
+
const icmpCode = buf[ihl + 1] ?? 0;
|
|
2425
|
+
const fromIp = buf.readUInt32LE(12);
|
|
2426
|
+
if (icmpType === 0) {
|
|
2427
|
+
if (buf.readUInt16BE(ihl + 4) !== expectedId) return null;
|
|
2428
|
+
return {
|
|
2429
|
+
type: icmpType,
|
|
2430
|
+
code: icmpCode,
|
|
2431
|
+
fromIp
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
if (icmpType === 11 || icmpType === 3) {
|
|
2435
|
+
const origStart = ihl + 8;
|
|
2436
|
+
if (n < origStart + 28) return null;
|
|
2437
|
+
const origIhl = ((buf[origStart] ?? 0) & 15) * 4;
|
|
2438
|
+
if (n < origStart + origIhl + 8) return null;
|
|
2439
|
+
if (buf.readUInt16BE(origStart + origIhl + 4) !== expectedId) return null;
|
|
2440
|
+
return {
|
|
2441
|
+
type: icmpType,
|
|
2442
|
+
code: icmpCode,
|
|
2443
|
+
fromIp
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
2448
|
+
function posixStatusLabel(type, code, timedOut) {
|
|
2449
|
+
if (type === 0) return "SUCCESS";
|
|
2450
|
+
if (timedOut) return "REQ_TIMED_OUT";
|
|
2451
|
+
if (type === 11 && code === 0) return "TTL_EXPIRED_TRANSIT";
|
|
2452
|
+
if (type === 11 && code === 1) return "TTL_EXPIRED_REASSEM";
|
|
2453
|
+
if (type === 3 && code === 0) return "DEST_NET_UNREACHABLE";
|
|
2454
|
+
if (type === 3 && code === 1) return "DEST_HOST_UNREACHABLE";
|
|
2455
|
+
if (type === 3 && code === 2) return "DEST_PROT_UNREACHABLE";
|
|
2456
|
+
if (type === 3 && code === 3) return "DEST_PORT_UNREACHABLE";
|
|
2457
|
+
return `UNKNOWN_${type}_${code}`;
|
|
2458
|
+
}
|
|
2459
|
+
function posixErrorClass(type, _code, timedOut) {
|
|
2460
|
+
if (type === 0) return "success";
|
|
2461
|
+
if (timedOut) return "timeout";
|
|
2462
|
+
if (type === 11) return "time_exceeded";
|
|
2463
|
+
if (type === 3) return "destination_unreachable";
|
|
2464
|
+
return "error";
|
|
2465
|
+
}
|
|
2466
|
+
function posixSetTtl(fd, ttl) {
|
|
2467
|
+
const buf = Buffer.alloc(4);
|
|
2468
|
+
buf.writeInt32LE(ttl);
|
|
2469
|
+
posixSetsockopt(fd, IPPROTO_IP, IP_TTL, buf, 4);
|
|
2470
|
+
}
|
|
2471
|
+
function posixSetRecvTimeout(fd, timeoutMs) {
|
|
2472
|
+
const tv = Buffer.alloc(16, 0);
|
|
2473
|
+
tv.writeInt32LE(Math.floor(timeoutMs / 1e3), 0);
|
|
2474
|
+
tv.writeInt32LE(timeoutMs % 1e3 * 1e3, 8);
|
|
2475
|
+
posixSetsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv, 16);
|
|
2476
|
+
}
|
|
2477
|
+
function posixIcmpProbe(params) {
|
|
2478
|
+
const { target, ttl = 128, packetSize = ICMP_DEFAULT_PACKET_SIZE, timeout = ICMP_PROBE_TIMEOUT_MS } = params;
|
|
2479
|
+
if (!isValidIpv4(target)) return {
|
|
2480
|
+
target,
|
|
2481
|
+
ip: "",
|
|
2482
|
+
alive: false,
|
|
2483
|
+
rtt: null,
|
|
2484
|
+
ttl,
|
|
2485
|
+
icmpStatus: "INVALID_ADDRESS",
|
|
2486
|
+
errorClass: "error",
|
|
2487
|
+
packetSize
|
|
2488
|
+
};
|
|
2489
|
+
const fd = posixSocket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
2490
|
+
if (fd < 0) return {
|
|
2491
|
+
target,
|
|
2492
|
+
ip: "",
|
|
2493
|
+
alive: false,
|
|
2494
|
+
rtt: null,
|
|
2495
|
+
ttl,
|
|
2496
|
+
icmpStatus: "SOCKET_ERROR",
|
|
2497
|
+
errorClass: "error",
|
|
2498
|
+
packetSize
|
|
2499
|
+
};
|
|
2500
|
+
try {
|
|
2501
|
+
posixSetTtl(fd, ttl);
|
|
2502
|
+
posixSetRecvTimeout(fd, timeout);
|
|
2503
|
+
const id = process.pid & 65535;
|
|
2504
|
+
const packet = buildIcmpEcho(id, 1, packetSize);
|
|
2505
|
+
const destAddr = buildSockaddrIn(target);
|
|
2506
|
+
const t0 = performance.now();
|
|
2507
|
+
if (posixSendto(fd, packet, destAddr) < 0) return {
|
|
2508
|
+
target,
|
|
2509
|
+
ip: target,
|
|
2510
|
+
alive: false,
|
|
2511
|
+
rtt: null,
|
|
2512
|
+
ttl,
|
|
2513
|
+
icmpStatus: "SEND_ERROR",
|
|
2514
|
+
errorClass: "error",
|
|
2515
|
+
packetSize
|
|
2516
|
+
};
|
|
2517
|
+
const recvBuf = Buffer.alloc(512);
|
|
2518
|
+
const n = posixRecv(fd, recvBuf);
|
|
2519
|
+
const rtt = Math.round(performance.now() - t0);
|
|
2520
|
+
if (n <= 0) return {
|
|
2521
|
+
target,
|
|
2522
|
+
ip: target,
|
|
2523
|
+
alive: false,
|
|
2524
|
+
rtt: null,
|
|
2525
|
+
ttl,
|
|
2526
|
+
icmpStatus: "REQ_TIMED_OUT",
|
|
2527
|
+
errorClass: "timeout",
|
|
2528
|
+
packetSize
|
|
2529
|
+
};
|
|
2530
|
+
const reply = parseIcmpPacket(recvBuf, n, id);
|
|
2531
|
+
if (!reply) return {
|
|
2532
|
+
target,
|
|
2533
|
+
ip: target,
|
|
2534
|
+
alive: false,
|
|
2535
|
+
rtt: null,
|
|
2536
|
+
ttl,
|
|
2537
|
+
icmpStatus: "UNEXPECTED_REPLY",
|
|
2538
|
+
errorClass: "error",
|
|
2539
|
+
packetSize
|
|
2540
|
+
};
|
|
2541
|
+
const alive = reply.type === 0;
|
|
2542
|
+
return {
|
|
2543
|
+
target,
|
|
2544
|
+
ip: ipToString(reply.fromIp),
|
|
2545
|
+
alive,
|
|
2546
|
+
rtt: alive ? rtt : null,
|
|
2547
|
+
ttl,
|
|
2548
|
+
icmpStatus: posixStatusLabel(reply.type, reply.code, false),
|
|
2549
|
+
errorClass: posixErrorClass(reply.type, reply.code, false),
|
|
2550
|
+
packetSize
|
|
2551
|
+
};
|
|
2552
|
+
} finally {
|
|
2553
|
+
posixClose(fd);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function posixTraceroute(params) {
|
|
2557
|
+
const { target, maxHops = ICMP_TRACEROUTE_MAX_HOPS, timeout = ICMP_PROBE_TIMEOUT_MS, packetSize = ICMP_DEFAULT_PACKET_SIZE } = params;
|
|
2558
|
+
if (!isValidIpv4(target)) return {
|
|
2559
|
+
target,
|
|
2560
|
+
ip: "",
|
|
2561
|
+
hops: [],
|
|
2562
|
+
reached: false,
|
|
2563
|
+
totalHops: 0,
|
|
2564
|
+
totalTime: 0
|
|
2565
|
+
};
|
|
2566
|
+
const fd = posixSocket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
2567
|
+
if (fd < 0) return {
|
|
2568
|
+
target,
|
|
2569
|
+
ip: "",
|
|
2570
|
+
hops: [],
|
|
2571
|
+
reached: false,
|
|
2572
|
+
totalHops: 0,
|
|
2573
|
+
totalTime: 0
|
|
2574
|
+
};
|
|
2575
|
+
const hops = [];
|
|
2576
|
+
const id = process.pid & 65535;
|
|
2577
|
+
const destAddr = buildSockaddrIn(target);
|
|
2578
|
+
const t0 = performance.now();
|
|
2579
|
+
const MAX_CONSECUTIVE_SEND_ERRORS = 5;
|
|
2580
|
+
let consecutiveSendErrors = 0;
|
|
2581
|
+
try {
|
|
2582
|
+
posixSetRecvTimeout(fd, timeout);
|
|
2583
|
+
for (let ttl = 1; ttl <= maxHops; ttl++) {
|
|
2584
|
+
posixSetTtl(fd, ttl);
|
|
2585
|
+
const packet = buildIcmpEcho(id, ttl, packetSize);
|
|
2586
|
+
const sendT0 = performance.now();
|
|
2587
|
+
if (posixSendto(fd, packet, destAddr) < 0) {
|
|
2588
|
+
consecutiveSendErrors++;
|
|
2589
|
+
hops.push({
|
|
2590
|
+
hop: ttl,
|
|
2591
|
+
ip: null,
|
|
2592
|
+
rtt: null,
|
|
2593
|
+
status: "SEND_ERROR",
|
|
2594
|
+
errorClass: "error"
|
|
2595
|
+
});
|
|
2596
|
+
if (consecutiveSendErrors >= MAX_CONSECUTIVE_SEND_ERRORS) break;
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
consecutiveSendErrors = 0;
|
|
2600
|
+
const recvBuf = Buffer.alloc(512);
|
|
2601
|
+
const n = posixRecv(fd, recvBuf);
|
|
2602
|
+
const rtt = Math.round(performance.now() - sendT0);
|
|
2603
|
+
if (n <= 0) {
|
|
2604
|
+
hops.push({
|
|
2605
|
+
hop: ttl,
|
|
2606
|
+
ip: null,
|
|
2607
|
+
rtt: null,
|
|
2608
|
+
status: "REQ_TIMED_OUT",
|
|
2609
|
+
errorClass: "timeout"
|
|
2610
|
+
});
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
const reply = parseIcmpPacket(recvBuf, n, id);
|
|
2614
|
+
if (!reply) {
|
|
2615
|
+
hops.push({
|
|
2616
|
+
hop: ttl,
|
|
2617
|
+
ip: null,
|
|
2618
|
+
rtt: null,
|
|
2619
|
+
status: "UNEXPECTED_REPLY",
|
|
2620
|
+
errorClass: "error"
|
|
2621
|
+
});
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
const status = posixStatusLabel(reply.type, reply.code, false);
|
|
2625
|
+
const errorCls = posixErrorClass(reply.type, reply.code, false);
|
|
2626
|
+
hops.push({
|
|
2627
|
+
hop: ttl,
|
|
2628
|
+
ip: ipToString(reply.fromIp),
|
|
2629
|
+
rtt,
|
|
2630
|
+
status,
|
|
2631
|
+
errorClass: errorCls
|
|
2632
|
+
});
|
|
2633
|
+
if (reply.type === 0) break;
|
|
2634
|
+
}
|
|
2635
|
+
} finally {
|
|
2636
|
+
posixClose(fd);
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
target,
|
|
2640
|
+
ip: target,
|
|
2641
|
+
hops,
|
|
2642
|
+
reached: hops[hops.length - 1]?.status === "SUCCESS",
|
|
2643
|
+
totalHops: hops.length,
|
|
2644
|
+
totalTime: Math.round((performance.now() - t0) * 100) / 100
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
const isPosix = process.platform === "linux" || process.platform === "darwin";
|
|
2648
|
+
function isIcmpAvailable() {
|
|
2649
|
+
if (_available !== null) return _available;
|
|
2650
|
+
if (process.platform === "win32") try {
|
|
2651
|
+
koffi.load("iphlpapi.dll").unload();
|
|
2652
|
+
_available = true;
|
|
2653
|
+
return true;
|
|
2654
|
+
} catch {
|
|
2655
|
+
_available = false;
|
|
2656
|
+
return false;
|
|
2657
|
+
}
|
|
2658
|
+
if (isPosix) {
|
|
2659
|
+
try {
|
|
2660
|
+
const fd = posixSocket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
2661
|
+
if (fd >= 0) {
|
|
2662
|
+
posixClose(fd);
|
|
2663
|
+
_available = true;
|
|
2664
|
+
} else _available = false;
|
|
2665
|
+
} catch {
|
|
2666
|
+
_available = false;
|
|
2667
|
+
}
|
|
2668
|
+
return _available;
|
|
2669
|
+
}
|
|
2670
|
+
_available = false;
|
|
2671
|
+
return false;
|
|
2672
|
+
}
|
|
2673
|
+
function icmpProbe(params) {
|
|
2674
|
+
const { target, ttl = 128, packetSize = ICMP_DEFAULT_PACKET_SIZE, timeout = ICMP_PROBE_TIMEOUT_MS } = params;
|
|
2675
|
+
if (!isIcmpAvailable()) return {
|
|
2676
|
+
target,
|
|
2677
|
+
ip: "",
|
|
2678
|
+
alive: false,
|
|
2679
|
+
rtt: null,
|
|
2680
|
+
ttl,
|
|
2681
|
+
icmpStatus: "PLATFORM_NOT_SUPPORTED",
|
|
2682
|
+
errorClass: "error",
|
|
2683
|
+
packetSize
|
|
2684
|
+
};
|
|
2685
|
+
if (process.platform === "win32") return winIcmpProbe({
|
|
2686
|
+
target,
|
|
2687
|
+
ttl,
|
|
2688
|
+
packetSize,
|
|
2689
|
+
timeout
|
|
2690
|
+
});
|
|
2691
|
+
return posixIcmpProbe({
|
|
2692
|
+
target,
|
|
2693
|
+
ttl,
|
|
2694
|
+
packetSize,
|
|
2695
|
+
timeout
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
function traceroute(params) {
|
|
2699
|
+
const { target, maxHops = ICMP_TRACEROUTE_MAX_HOPS, timeout = ICMP_PROBE_TIMEOUT_MS, packetSize = ICMP_DEFAULT_PACKET_SIZE } = params;
|
|
2700
|
+
if (!isIcmpAvailable()) return {
|
|
2701
|
+
target,
|
|
2702
|
+
ip: "",
|
|
2703
|
+
hops: [],
|
|
2704
|
+
reached: false,
|
|
2705
|
+
totalHops: 0,
|
|
2706
|
+
totalTime: 0
|
|
2707
|
+
};
|
|
2708
|
+
if (process.platform === "win32") return winTraceroute({
|
|
2709
|
+
target,
|
|
2710
|
+
maxHops,
|
|
2711
|
+
timeout,
|
|
2712
|
+
packetSize
|
|
2713
|
+
});
|
|
2714
|
+
return posixTraceroute({
|
|
2715
|
+
target,
|
|
2716
|
+
maxHops,
|
|
2717
|
+
timeout,
|
|
2718
|
+
packetSize
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
//#endregion
|
|
2722
|
+
//#region src/server/domains/network/handlers/raw-handlers.ts
|
|
2723
|
+
/**
|
|
2724
|
+
* Raw HTTP/HTTP2/DNS/RTT handlers — standalone class using composition.
|
|
2725
|
+
*
|
|
2726
|
+
* Extracted from AdvancedToolHandlersRaw (handlers.impl.core.runtime.raw.ts).
|
|
2727
|
+
* Uses helpers from ./raw-helpers.ts and ./shared.ts instead of inheritance.
|
|
2728
|
+
*/
|
|
2729
|
+
var RawHandlers = class {
|
|
2730
|
+
constructor(eventBus) {
|
|
2731
|
+
this.eventBus = eventBus;
|
|
2732
|
+
}
|
|
2733
|
+
async handleDnsResolve(args) {
|
|
2734
|
+
try {
|
|
2735
|
+
const hostname = parseOptionalString(args.hostname, "hostname");
|
|
2736
|
+
if (!hostname) return R.text("hostname is required", true);
|
|
2737
|
+
const rrType = parseOptionalString(args.rrType, "rrType") ?? "A";
|
|
2738
|
+
const validTypes = [
|
|
2739
|
+
"A",
|
|
2740
|
+
"AAAA",
|
|
2741
|
+
"MX",
|
|
2742
|
+
"TXT",
|
|
2743
|
+
"NS",
|
|
2744
|
+
"CNAME",
|
|
2745
|
+
"SOA",
|
|
2746
|
+
"PTR",
|
|
2747
|
+
"SRV",
|
|
2748
|
+
"ANY"
|
|
2749
|
+
];
|
|
2750
|
+
if (!validTypes.includes(rrType)) return R.text(`Invalid rrType: "${rrType}". Expected one of: ${validTypes.join(", ")}`, true);
|
|
2751
|
+
const start = performance.now();
|
|
2752
|
+
const records = await dns.resolve(hostname, rrType);
|
|
2753
|
+
const timing = roundMs(performance.now() - start);
|
|
2754
|
+
return R.ok().json({
|
|
2755
|
+
hostname,
|
|
2756
|
+
rrType,
|
|
2757
|
+
records,
|
|
2758
|
+
timing
|
|
2759
|
+
});
|
|
2760
|
+
} catch (err) {
|
|
2761
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2762
|
+
return R.fail(`DNS resolve failed: ${message}`).json();
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
async handleDnsReverse(args) {
|
|
2766
|
+
try {
|
|
2767
|
+
const ip = parseOptionalString(args.ip, "ip");
|
|
2768
|
+
if (!ip) return R.text("ip is required", true);
|
|
2769
|
+
const start = performance.now();
|
|
2770
|
+
const hostnames = await dns.reverse(ip);
|
|
2771
|
+
const timing = roundMs(performance.now() - start);
|
|
2772
|
+
return R.ok().json({
|
|
2773
|
+
ip,
|
|
2774
|
+
hostnames,
|
|
2775
|
+
timing
|
|
2776
|
+
});
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2779
|
+
return R.fail(`DNS reverse lookup failed: ${message}`).json();
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
async handleHttpRequestBuild(args) {
|
|
2783
|
+
try {
|
|
2784
|
+
const method = parseOptionalString(args.method, "method");
|
|
2785
|
+
const target = parseOptionalString(args.target, "target");
|
|
2786
|
+
if (!method) throw new Error("method is required");
|
|
2787
|
+
if (!target) throw new Error("target is required");
|
|
2788
|
+
const built = buildHttpRequest({
|
|
2789
|
+
method,
|
|
2790
|
+
target,
|
|
2791
|
+
host: parseOptionalString(args.host, "host"),
|
|
2792
|
+
headers: parseHeaderRecord(args.headers, "headers"),
|
|
2793
|
+
body: parseRawString(args.body, "body", { allowEmpty: true }),
|
|
2794
|
+
httpVersion: parseOptionalString(args.httpVersion, "httpVersion") ?? "1.1",
|
|
2795
|
+
addHostHeader: parseBooleanArg(args.addHostHeader, true),
|
|
2796
|
+
addContentLength: parseBooleanArg(args.addContentLength, true),
|
|
2797
|
+
addConnectionClose: parseBooleanArg(args.addConnectionClose, true)
|
|
2798
|
+
});
|
|
2799
|
+
emitEvent(this.eventBus, "network:http_request_built", {
|
|
2800
|
+
method: built.startLine.split(" ", 1)[0] ?? "UNKNOWN",
|
|
2801
|
+
target,
|
|
2802
|
+
byteLength: built.requestBytes,
|
|
2803
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2804
|
+
});
|
|
2805
|
+
return R.ok().merge(built).json();
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
async handleHttpPlainRequest(args) {
|
|
2811
|
+
try {
|
|
2812
|
+
const hostArg = parseOptionalString(args.host, "host");
|
|
2813
|
+
const requestText = parseRawString(args.requestText, "requestText");
|
|
2814
|
+
if (!hostArg) throw new Error("host is required");
|
|
2815
|
+
if (!requestText) throw new Error("requestText is required");
|
|
2816
|
+
const host = normalizeTargetHost(hostArg);
|
|
2817
|
+
const port = parseNumberArg(args.port, {
|
|
2818
|
+
defaultValue: 80,
|
|
2819
|
+
min: 1,
|
|
2820
|
+
max: 65535,
|
|
2821
|
+
integer: true
|
|
2822
|
+
});
|
|
2823
|
+
const timeoutMs = parseNumberArg(args.timeoutMs, {
|
|
2824
|
+
defaultValue: 3e4,
|
|
2825
|
+
min: 1,
|
|
2826
|
+
max: 12e4,
|
|
2827
|
+
integer: true
|
|
2828
|
+
});
|
|
2829
|
+
const maxResponseBytes = parseNumberArg(args.maxResponseBytes, {
|
|
2830
|
+
defaultValue: 512e3,
|
|
2831
|
+
min: 256,
|
|
2832
|
+
max: 5242880,
|
|
2833
|
+
integer: true
|
|
2834
|
+
});
|
|
2835
|
+
const requestMethod = getRequestMethod(requestText);
|
|
2836
|
+
const authorization = parseNetworkAuthorization(args.authorization);
|
|
2837
|
+
const { target } = await resolveAuthorizedTransportTarget(`http://${formatHostForUrl(host)}:${String(port)}/`, authorization, "HTTP request");
|
|
2838
|
+
if (authorization) {
|
|
2839
|
+
const requestTarget = (requestText.split(/\r?\n/, 1)[0] ?? "").split(/\s+/)[1] ?? "";
|
|
2840
|
+
if (requestTarget.includes("://")) try {
|
|
2841
|
+
const targetHost = normalizeTargetHost(new URL(requestTarget).hostname);
|
|
2842
|
+
if (targetHost !== host && targetHost !== (target.resolvedAddress ?? "")) throw new Error(`HTTP request blocked: request-line target host "${targetHost}" does not match authorized host "${host}"`);
|
|
2843
|
+
} catch (e) {
|
|
2844
|
+
if (e instanceof Error && e.message.startsWith("HTTP request blocked:")) throw e;
|
|
2845
|
+
}
|
|
2846
|
+
const hostHeaderValue = requestText.match(/^Host:\s*(\S+)/im)?.[1];
|
|
2847
|
+
if (hostHeaderValue) {
|
|
2848
|
+
const declaredHost = normalizeTargetHost(hostHeaderValue.replace(/:\d+$/, ""));
|
|
2849
|
+
if (declaredHost !== host && declaredHost !== (target.resolvedAddress ?? "")) throw new Error(`HTTP request blocked: Host header "${hostHeaderValue}" does not match authorized host "${host}"`);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
const exchange = await exchangePlainHttp(target.resolvedAddress ?? target.hostname, port, Buffer.from(requestText, "utf8"), requestMethod, timeoutMs, maxResponseBytes);
|
|
2853
|
+
const analysis = analyzeHttpResponse(exchange.rawResponse, requestMethod);
|
|
2854
|
+
if (!analysis) throw new Error("Received data but could not parse complete HTTP response headers.");
|
|
2855
|
+
const bodyIsText = isLikelyTextHttpBody(analysis.rawHeaders.find((h) => h.name.toLowerCase() === "content-type")?.value ?? null, analysis.bodyBuffer);
|
|
2856
|
+
const complete = analysis.complete || analysis.bodyMode === "until-close" && exchange.endedBy === "socket-close";
|
|
2857
|
+
emitEvent(this.eventBus, "network:http_plain_request_completed", {
|
|
2858
|
+
host,
|
|
2859
|
+
port,
|
|
2860
|
+
statusCode: analysis.statusCode,
|
|
2861
|
+
byteLength: exchange.rawResponse.length,
|
|
2862
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2863
|
+
});
|
|
2864
|
+
return R.ok().merge({
|
|
2865
|
+
host,
|
|
2866
|
+
port,
|
|
2867
|
+
resolvedAddress: target.resolvedAddress ?? target.hostname,
|
|
2868
|
+
requestBytes: Buffer.byteLength(requestText, "utf8"),
|
|
2869
|
+
response: {
|
|
2870
|
+
statusLine: analysis.statusLine,
|
|
2871
|
+
httpVersion: analysis.httpVersion,
|
|
2872
|
+
statusCode: analysis.statusCode,
|
|
2873
|
+
statusText: analysis.statusText,
|
|
2874
|
+
headers: analysis.headers,
|
|
2875
|
+
rawHeaders: analysis.rawHeaders,
|
|
2876
|
+
headerBytes: analysis.headerBytes,
|
|
2877
|
+
bodyBytes: analysis.bodyBytes,
|
|
2878
|
+
bodyMode: analysis.bodyMode,
|
|
2879
|
+
chunkedDecoded: analysis.chunkedDecoded,
|
|
2880
|
+
complete,
|
|
2881
|
+
truncated: exchange.endedBy === "max-bytes",
|
|
2882
|
+
endedBy: exchange.endedBy,
|
|
2883
|
+
bodyText: bodyIsText ? analysis.bodyBuffer.toString("utf8") : void 0,
|
|
2884
|
+
bodyBase64: bodyIsText ? void 0 : analysis.bodyBuffer.toString("base64")
|
|
2885
|
+
}
|
|
2886
|
+
}).json();
|
|
2887
|
+
} catch (error) {
|
|
2888
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
async handleHttp2Probe(args) {
|
|
2892
|
+
const rawUrl = parseOptionalString(args.url, "url");
|
|
2893
|
+
let eventUrl = rawUrl ?? "";
|
|
2894
|
+
let eventStatusCode = null;
|
|
2895
|
+
let eventAlpnProtocol = null;
|
|
2896
|
+
let eventSuccess = false;
|
|
2897
|
+
try {
|
|
2898
|
+
if (!rawUrl) throw new Error("url is required");
|
|
2899
|
+
const method = (parseOptionalString(args.method, "method") ?? "GET").toUpperCase();
|
|
2900
|
+
if (!HTTP_TOKEN_RE.test(method)) throw new Error("method must be a valid HTTP token");
|
|
2901
|
+
const timeoutMs = parseNumberArg(args.timeoutMs, {
|
|
2902
|
+
defaultValue: 3e4,
|
|
2903
|
+
min: 1,
|
|
2904
|
+
max: 12e4,
|
|
2905
|
+
integer: true
|
|
2906
|
+
});
|
|
2907
|
+
const maxBodyBytes = parseNumberArg(args.maxBodyBytes, {
|
|
2908
|
+
defaultValue: 32768,
|
|
2909
|
+
min: 128,
|
|
2910
|
+
max: 1048576,
|
|
2911
|
+
integer: true
|
|
2912
|
+
});
|
|
2913
|
+
const bodyBuffer = Buffer.from(parseRawString(args.body, "body", { allowEmpty: true }) ?? "", "utf8");
|
|
2914
|
+
const alpnProtocols = parseStringArray(args.alpnProtocols, "alpnProtocols");
|
|
2915
|
+
const requestHeaders = toHttp2RequestHeaders(parseHeaderRecord(args.headers, "headers"));
|
|
2916
|
+
const { url, target } = await resolveAuthorizedTransportTarget(rawUrl, parseNetworkAuthorization(args.authorization), "HTTP/2 probe");
|
|
2917
|
+
eventUrl = url.toString();
|
|
2918
|
+
if (!("content-length" in requestHeaders) && bodyBuffer.length > 0) requestHeaders["content-length"] = String(bodyBuffer.length);
|
|
2919
|
+
const { responseHeaders, bodyBuffer: capturedBody, truncated, alpnProtocol } = await performHttp2ProbeInternal({
|
|
2920
|
+
url,
|
|
2921
|
+
target,
|
|
2922
|
+
method,
|
|
2923
|
+
requestHeaders,
|
|
2924
|
+
bodyBuffer,
|
|
2925
|
+
timeoutMs,
|
|
2926
|
+
maxBodyBytes,
|
|
2927
|
+
effectivePort: Number.parseInt(url.port || (url.protocol === "https:" ? "443" : "80"), 10),
|
|
2928
|
+
requestedAlpnProtocols: alpnProtocols.length > 0 ? alpnProtocols : ["h2", "http/1.1"]
|
|
2929
|
+
});
|
|
2930
|
+
const normalizedHeaders = normalizeHttp2Headers(responseHeaders);
|
|
2931
|
+
const rawStatus = responseHeaders[":status"];
|
|
2932
|
+
const statusCode = typeof rawStatus === "number" ? rawStatus : typeof rawStatus === "string" ? Number.parseInt(rawStatus, 10) : null;
|
|
2933
|
+
const bodyIsText = isLikelyTextHttpBody(typeof normalizedHeaders["content-type"] === "string" ? normalizedHeaders["content-type"] : Array.isArray(normalizedHeaders["content-type"]) ? normalizedHeaders["content-type"][0] ?? null : null, capturedBody);
|
|
2934
|
+
eventStatusCode = Number.isFinite(statusCode ?? NaN) ? statusCode : null;
|
|
2935
|
+
eventAlpnProtocol = alpnProtocol;
|
|
2936
|
+
eventSuccess = true;
|
|
2937
|
+
return R.ok().merge({
|
|
2938
|
+
url: eventUrl,
|
|
2939
|
+
statusCode: eventStatusCode,
|
|
2940
|
+
alpnProtocol: eventAlpnProtocol,
|
|
2941
|
+
headers: normalizedHeaders,
|
|
2942
|
+
bodyBytes: capturedBody.length,
|
|
2943
|
+
truncated,
|
|
2944
|
+
bodyText: bodyIsText ? capturedBody.toString("utf8") : void 0,
|
|
2945
|
+
bodyBase64: bodyIsText ? void 0 : capturedBody.toString("base64")
|
|
2946
|
+
}).json();
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
2949
|
+
} finally {
|
|
2950
|
+
emitEvent(this.eventBus, "network:http2_probed", {
|
|
2951
|
+
url: eventUrl,
|
|
2952
|
+
success: eventSuccess,
|
|
2953
|
+
statusCode: eventStatusCode,
|
|
2954
|
+
alpnProtocol: eventAlpnProtocol,
|
|
2955
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
async handleHttp2FrameBuild(args) {
|
|
2960
|
+
const frameTypeRaw = parseOptionalString(args.frameType, "frameType");
|
|
2961
|
+
if (!frameTypeRaw) throw new Error("frameType is required");
|
|
2962
|
+
const validFrameTypes = [
|
|
2963
|
+
"DATA",
|
|
2964
|
+
"SETTINGS",
|
|
2965
|
+
"PING",
|
|
2966
|
+
"WINDOW_UPDATE",
|
|
2967
|
+
"RST_STREAM",
|
|
2968
|
+
"GOAWAY",
|
|
2969
|
+
"RAW"
|
|
2970
|
+
];
|
|
2971
|
+
const frameType = frameTypeRaw.toUpperCase();
|
|
2972
|
+
if (!validFrameTypes.includes(frameType)) throw new Error(`frameType must be one of: ${validFrameTypes.join(", ")}`);
|
|
2973
|
+
const streamId = args.streamId !== void 0 ? parseNumberArg(args.streamId, {
|
|
2974
|
+
defaultValue: 0,
|
|
2975
|
+
min: 0,
|
|
2976
|
+
integer: true
|
|
2977
|
+
}) : void 0;
|
|
2978
|
+
const flags = args.flags !== void 0 ? parseNumberArg(args.flags, {
|
|
2979
|
+
defaultValue: 0,
|
|
2980
|
+
min: 0,
|
|
2981
|
+
max: 255,
|
|
2982
|
+
integer: true
|
|
2983
|
+
}) : void 0;
|
|
2984
|
+
const frameTypeCode = args.frameTypeCode !== void 0 ? parseNumberArg(args.frameTypeCode, {
|
|
2985
|
+
defaultValue: 0,
|
|
2986
|
+
min: 0,
|
|
2987
|
+
max: 255,
|
|
2988
|
+
integer: true
|
|
2989
|
+
}) : void 0;
|
|
2990
|
+
const windowSizeIncrement = args.windowSizeIncrement !== void 0 ? parseNumberArg(args.windowSizeIncrement, {
|
|
2991
|
+
defaultValue: 1,
|
|
2992
|
+
min: 1,
|
|
2993
|
+
integer: true
|
|
2994
|
+
}) : void 0;
|
|
2995
|
+
const errorCode = args.errorCode !== void 0 ? parseNumberArg(args.errorCode, {
|
|
2996
|
+
defaultValue: 0,
|
|
2997
|
+
min: 0,
|
|
2998
|
+
integer: true
|
|
2999
|
+
}) : void 0;
|
|
3000
|
+
const lastStreamId = args.lastStreamId !== void 0 ? parseNumberArg(args.lastStreamId, {
|
|
3001
|
+
defaultValue: 0,
|
|
3002
|
+
min: 0,
|
|
3003
|
+
integer: true
|
|
3004
|
+
}) : void 0;
|
|
3005
|
+
const payloadHex = parseOptionalString(args.payloadHex, "payloadHex");
|
|
3006
|
+
const payloadText = parseRawString(args.payloadText, "payloadText", { allowEmpty: true });
|
|
3007
|
+
const payloadEncoding = parseOptionalString(args.payloadEncoding, "payloadEncoding");
|
|
3008
|
+
const ack = parseOptionalBoolean(args.ack, "ack");
|
|
3009
|
+
const pingOpaqueDataHex = parseOptionalString(args.pingOpaqueDataHex, "pingOpaqueDataHex");
|
|
3010
|
+
const debugDataText = parseRawString(args.debugDataText, "debugDataText", { allowEmpty: true });
|
|
3011
|
+
const debugDataEncoding = parseOptionalString(args.debugDataEncoding, "debugDataEncoding");
|
|
3012
|
+
let settings;
|
|
3013
|
+
if (args.settings !== void 0) {
|
|
3014
|
+
if (!Array.isArray(args.settings)) throw new Error("settings must be an array");
|
|
3015
|
+
settings = args.settings.map((entry, index) => {
|
|
3016
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`settings[${String(index)}] must be an object with id and value`);
|
|
3017
|
+
return {
|
|
3018
|
+
id: typeof entry.id === "number" ? entry.id : (() => {
|
|
3019
|
+
throw new Error(`settings[${String(index)}].id must be a number`);
|
|
3020
|
+
})(),
|
|
3021
|
+
value: typeof entry.value === "number" ? entry.value : (() => {
|
|
3022
|
+
throw new Error(`settings[${String(index)}].value must be a number`);
|
|
3023
|
+
})()
|
|
3024
|
+
};
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
const result = buildHttp2Frame({
|
|
3028
|
+
frameType,
|
|
3029
|
+
...streamId !== void 0 && { streamId },
|
|
3030
|
+
...flags !== void 0 && { flags },
|
|
3031
|
+
...frameTypeCode !== void 0 && { frameTypeCode },
|
|
3032
|
+
...payloadHex !== void 0 && { payloadHex },
|
|
3033
|
+
...payloadText !== void 0 && { payloadText },
|
|
3034
|
+
...payloadEncoding !== void 0 && { payloadEncoding },
|
|
3035
|
+
...settings !== void 0 && { settings },
|
|
3036
|
+
...ack !== void 0 && { ack },
|
|
3037
|
+
...pingOpaqueDataHex !== void 0 && { pingOpaqueDataHex },
|
|
3038
|
+
...windowSizeIncrement !== void 0 && { windowSizeIncrement },
|
|
3039
|
+
...errorCode !== void 0 && { errorCode },
|
|
3040
|
+
...lastStreamId !== void 0 && { lastStreamId },
|
|
3041
|
+
...debugDataText !== void 0 && { debugDataText },
|
|
3042
|
+
...debugDataEncoding !== void 0 && { debugDataEncoding }
|
|
3043
|
+
});
|
|
3044
|
+
emitEvent(this.eventBus, "network:http2_frame_build_completed", {
|
|
3045
|
+
frameType: result.frameType,
|
|
3046
|
+
typeCode: result.typeCode,
|
|
3047
|
+
streamId: result.streamId,
|
|
3048
|
+
flags: result.flags,
|
|
3049
|
+
payloadBytes: result.payloadBytes,
|
|
3050
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3051
|
+
});
|
|
3052
|
+
return R.ok().merge(result).json();
|
|
3053
|
+
}
|
|
3054
|
+
async handleNetworkRttMeasure(args) {
|
|
3055
|
+
const urlRaw = parseOptionalString(args.url, "url");
|
|
3056
|
+
if (!urlRaw) throw new Error("url is required");
|
|
3057
|
+
const probeType = parseOptionalString(args.probeType, "probeType") ?? "tcp";
|
|
3058
|
+
if (![
|
|
3059
|
+
"tcp",
|
|
3060
|
+
"tls",
|
|
3061
|
+
"http"
|
|
3062
|
+
].includes(probeType)) throw new Error("probeType must be one of: tcp, tls, http");
|
|
3063
|
+
const iterations = clamp(args.iterations !== void 0 ? parseNumberArg(args.iterations, {
|
|
3064
|
+
defaultValue: 5,
|
|
3065
|
+
min: 1,
|
|
3066
|
+
integer: true
|
|
3067
|
+
}) : 5, 1, 50);
|
|
3068
|
+
const timeoutMs = clamp(args.timeoutMs !== void 0 ? parseNumberArg(args.timeoutMs, {
|
|
3069
|
+
defaultValue: 5e3,
|
|
3070
|
+
min: 100,
|
|
3071
|
+
integer: true
|
|
3072
|
+
}) : 5e3, 100, 3e4);
|
|
3073
|
+
const { url, target } = await resolveAuthorizedTransportTarget(urlRaw, parseNetworkAuthorization(args.authorization), "RTT measurement");
|
|
3074
|
+
const hostname = target.hostname;
|
|
3075
|
+
const port = Number(url.port) || (url.protocol === "https:" ? 443 : 80);
|
|
3076
|
+
const resolvedIp = target.resolvedAddress ?? hostname;
|
|
3077
|
+
const useHttps = url.protocol === "https:";
|
|
3078
|
+
const samples = [];
|
|
3079
|
+
const errors = [];
|
|
3080
|
+
for (let i = 0; i < iterations; i++) try {
|
|
3081
|
+
const rtt = await this.measureSingleRtt(hostname, resolvedIp, port, probeType, timeoutMs, useHttps);
|
|
3082
|
+
samples.push(rtt);
|
|
3083
|
+
} catch (err) {
|
|
3084
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
3085
|
+
}
|
|
3086
|
+
const stats = computeRttStats(samples);
|
|
3087
|
+
emitEvent(this.eventBus, "network:rtt_measured", {
|
|
3088
|
+
url: urlRaw,
|
|
3089
|
+
probeType,
|
|
3090
|
+
iterations,
|
|
3091
|
+
successCount: samples.length,
|
|
3092
|
+
errorCount: errors.length,
|
|
3093
|
+
stats,
|
|
3094
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3095
|
+
});
|
|
3096
|
+
return R.ok().merge({
|
|
3097
|
+
target: {
|
|
3098
|
+
hostname,
|
|
3099
|
+
port,
|
|
3100
|
+
resolvedIp,
|
|
3101
|
+
probeType
|
|
3102
|
+
},
|
|
3103
|
+
stats,
|
|
3104
|
+
samples,
|
|
3105
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
3106
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3107
|
+
}).json();
|
|
3108
|
+
}
|
|
3109
|
+
measureSingleRtt(hostname, address, port, probeType, timeoutMs, useHttps) {
|
|
3110
|
+
switch (probeType) {
|
|
3111
|
+
case "tcp": return this.probeTcp(address, port, timeoutMs);
|
|
3112
|
+
case "tls": return this.probeTls(hostname, address, port, timeoutMs);
|
|
3113
|
+
case "http": return this.probeHttp(hostname, address, port, timeoutMs, useHttps);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
createPinnedLookup(address) {
|
|
3117
|
+
const family = net.isIP(address) === 6 ? 6 : 4;
|
|
3118
|
+
return (_hostname, optionsOrCallback, maybeCallback) => {
|
|
3119
|
+
(typeof optionsOrCallback === "function" ? optionsOrCallback : maybeCallback)?.(null, address, family);
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
probeTcp(host, port, timeoutMs) {
|
|
3123
|
+
return new Promise((resolve, reject) => {
|
|
3124
|
+
const start = performance.now();
|
|
3125
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`TCP probe timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
3126
|
+
const socket = net.createConnection({
|
|
3127
|
+
host,
|
|
3128
|
+
port
|
|
3129
|
+
}, () => {
|
|
3130
|
+
clearTimeout(timer);
|
|
3131
|
+
socket.destroy();
|
|
3132
|
+
resolve(roundMs(performance.now() - start));
|
|
3133
|
+
});
|
|
3134
|
+
socket.on("error", (err) => {
|
|
3135
|
+
clearTimeout(timer);
|
|
3136
|
+
socket.destroy();
|
|
3137
|
+
reject(err);
|
|
3138
|
+
});
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
probeTls(hostname, address, port, timeoutMs) {
|
|
3142
|
+
return new Promise((resolve, reject) => {
|
|
3143
|
+
const start = performance.now();
|
|
3144
|
+
let settled = false;
|
|
3145
|
+
let socket = null;
|
|
3146
|
+
const finish = (callback) => {
|
|
3147
|
+
if (settled) return;
|
|
3148
|
+
settled = true;
|
|
3149
|
+
clearTimeout(timer);
|
|
3150
|
+
socket?.destroy();
|
|
3151
|
+
callback();
|
|
3152
|
+
};
|
|
3153
|
+
const timer = setTimeout(() => {
|
|
3154
|
+
finish(() => reject(/* @__PURE__ */ new Error(`TLS probe timed out after ${timeoutMs}ms`)));
|
|
3155
|
+
}, timeoutMs);
|
|
3156
|
+
socket = tls.connect({
|
|
3157
|
+
host: hostname,
|
|
3158
|
+
port,
|
|
3159
|
+
lookup: this.createPinnedLookup(address),
|
|
3160
|
+
...net.isIP(hostname) === 0 ? { servername: hostname } : {}
|
|
3161
|
+
}, () => {
|
|
3162
|
+
finish(() => resolve(roundMs(performance.now() - start)));
|
|
3163
|
+
});
|
|
3164
|
+
socket.on("error", (err) => {
|
|
3165
|
+
finish(() => reject(err));
|
|
3166
|
+
});
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
probeHttp(hostname, address, port, timeoutMs, useHttps) {
|
|
3170
|
+
return new Promise((resolve, reject) => {
|
|
3171
|
+
const start = performance.now();
|
|
3172
|
+
let settled = false;
|
|
3173
|
+
let request = null;
|
|
3174
|
+
const finish = (callback) => {
|
|
3175
|
+
if (settled) return;
|
|
3176
|
+
settled = true;
|
|
3177
|
+
clearTimeout(timer);
|
|
3178
|
+
request?.destroy();
|
|
3179
|
+
callback();
|
|
3180
|
+
};
|
|
3181
|
+
const timer = setTimeout(() => {
|
|
3182
|
+
finish(() => reject(/* @__PURE__ */ new Error(`HTTP probe timed out after ${timeoutMs}ms`)));
|
|
3183
|
+
}, timeoutMs);
|
|
3184
|
+
request = (useHttps ? https.request : http$1.request)({
|
|
3185
|
+
host: hostname,
|
|
3186
|
+
port,
|
|
3187
|
+
path: "/",
|
|
3188
|
+
method: "HEAD",
|
|
3189
|
+
lookup: this.createPinnedLookup(address),
|
|
3190
|
+
...useHttps && net.isIP(hostname) === 0 ? { servername: hostname } : {}
|
|
3191
|
+
}, (response) => {
|
|
3192
|
+
response.resume();
|
|
3193
|
+
finish(() => resolve(roundMs(performance.now() - start)));
|
|
3194
|
+
});
|
|
3195
|
+
request.on("error", (error) => {
|
|
3196
|
+
finish(() => reject(error));
|
|
3197
|
+
});
|
|
3198
|
+
request.end();
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
async handleNetworkTraceroute(args) {
|
|
3202
|
+
try {
|
|
3203
|
+
if (!isIcmpAvailable()) return R.text("ICMP traceroute not available on this platform (Windows: native API, Linux/macOS: requires root/CAP_NET_RAW)", true);
|
|
3204
|
+
const target = parseOptionalString(args.target, "target");
|
|
3205
|
+
if (!target) return R.text("target is required", true);
|
|
3206
|
+
const result = traceroute({
|
|
3207
|
+
target,
|
|
3208
|
+
maxHops: clamp(args.maxHops !== void 0 ? Number(args.maxHops) : 30, 1, 64),
|
|
3209
|
+
timeout: clamp(args.timeout !== void 0 ? Number(args.timeout) : 5e3, 100, 3e4),
|
|
3210
|
+
packetSize: clamp(args.packetSize !== void 0 ? Number(args.packetSize) : 32, 8, 65500)
|
|
3211
|
+
});
|
|
3212
|
+
return R.ok().json(result);
|
|
3213
|
+
} catch (err) {
|
|
3214
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3215
|
+
return R.fail(`Traceroute failed: ${message}`).json();
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async handleNetworkIcmpProbe(args) {
|
|
3219
|
+
try {
|
|
3220
|
+
if (!isIcmpAvailable()) return R.text("ICMP probe not available on this platform (Windows: native API, Linux/macOS: requires root/CAP_NET_RAW)", true);
|
|
3221
|
+
const target = parseOptionalString(args.target, "target");
|
|
3222
|
+
if (!target) return R.text("target is required", true);
|
|
3223
|
+
const ttl = clamp(args.ttl !== void 0 ? Number(args.ttl) : 128, 1, 255);
|
|
3224
|
+
const timeout = clamp(args.timeout !== void 0 ? Number(args.timeout) : 5e3, 100, 3e4);
|
|
3225
|
+
const result = icmpProbe({
|
|
3226
|
+
target,
|
|
3227
|
+
ttl,
|
|
3228
|
+
packetSize: clamp(args.packetSize !== void 0 ? Number(args.packetSize) : 32, 8, 65500),
|
|
3229
|
+
timeout
|
|
3230
|
+
});
|
|
3231
|
+
return R.ok().json(result);
|
|
3232
|
+
} catch (err) {
|
|
3233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3234
|
+
return R.fail(`ICMP probe failed: ${message}`).json();
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
//#endregion
|
|
3239
|
+
//#region src/server/domains/network/handlers.impl.ts
|
|
3240
|
+
var AdvancedToolHandlers = class {
|
|
3241
|
+
collector;
|
|
3242
|
+
consoleMonitor;
|
|
3243
|
+
eventBus;
|
|
3244
|
+
performanceMonitor = null;
|
|
3245
|
+
detailedDataManager = DetailedDataManager.getInstance();
|
|
3246
|
+
core;
|
|
3247
|
+
perf;
|
|
3248
|
+
console_;
|
|
3249
|
+
replay;
|
|
3250
|
+
intercept;
|
|
3251
|
+
raw;
|
|
3252
|
+
constructor(collector, consoleMonitor, eventBus) {
|
|
3253
|
+
this.collector = collector;
|
|
3254
|
+
this.consoleMonitor = consoleMonitor;
|
|
3255
|
+
this.eventBus = eventBus;
|
|
3256
|
+
this.core = new CoreHandlers({
|
|
3257
|
+
collector,
|
|
3258
|
+
consoleMonitor,
|
|
3259
|
+
eventBus
|
|
3260
|
+
});
|
|
3261
|
+
this.perf = new PerformanceHandlers({
|
|
3262
|
+
collector,
|
|
3263
|
+
getPerformanceMonitor: () => this.getPerformanceMonitor()
|
|
3264
|
+
});
|
|
3265
|
+
this.console_ = new ConsoleHandlers({ consoleMonitor });
|
|
3266
|
+
this.replay = new ReplayHandlers({ consoleMonitor });
|
|
3267
|
+
this.intercept = new InterceptHandlers({
|
|
3268
|
+
consoleMonitor,
|
|
3269
|
+
eventBus
|
|
3270
|
+
});
|
|
3271
|
+
this.raw = new RawHandlers(eventBus);
|
|
3272
|
+
}
|
|
3273
|
+
getPerformanceMonitor() {
|
|
3274
|
+
if (!this.performanceMonitor) this.performanceMonitor = new PerformanceMonitor(this.collector);
|
|
3275
|
+
return this.performanceMonitor;
|
|
3276
|
+
}
|
|
3277
|
+
handleNetworkEnable = (args) => this.core.handleNetworkEnable(args);
|
|
3278
|
+
handleNetworkDisable = (args) => this.core.handleNetworkDisable(args);
|
|
3279
|
+
handleNetworkGetStatus = (args) => this.core.handleNetworkGetStatus(args);
|
|
3280
|
+
handleNetworkMonitor = (args) => this.core.handleNetworkMonitor(args);
|
|
3281
|
+
handleNetworkGetRequests = (args) => this.core.handleNetworkGetRequests(args);
|
|
3282
|
+
handleNetworkGetResponseBody = (args) => this.core.handleNetworkGetResponseBody(args);
|
|
3283
|
+
handleNetworkGetStats = (args) => this.core.handleNetworkGetStats(args);
|
|
3284
|
+
handlePerformanceGetMetrics = (args) => this.perf.handlePerformanceGetMetrics(args);
|
|
3285
|
+
handlePerformanceCoverage = (args) => this.perf.handlePerformanceCoverage(args);
|
|
3286
|
+
handlePerformanceStartCoverage = (args) => this.perf.handlePerformanceStartCoverage(args);
|
|
3287
|
+
handlePerformanceStopCoverage = (args) => this.perf.handlePerformanceStopCoverage(args);
|
|
3288
|
+
handlePerformanceTakeHeapSnapshot = (args) => this.perf.handlePerformanceTakeHeapSnapshot(args);
|
|
3289
|
+
handlePerformanceTraceStart = (args) => this.perf.handlePerformanceTraceStart(args);
|
|
3290
|
+
handlePerformanceTraceStop = (args) => this.perf.handlePerformanceTraceStop(args);
|
|
3291
|
+
handlePerformanceTraceDispatch = (args) => String(args["action"] ?? "") === "stop" ? this.perf.handlePerformanceTraceStop(args) : this.perf.handlePerformanceTraceStart(args);
|
|
3292
|
+
handleProfilerCpuStart = (args) => this.perf.handleProfilerCpuStart(args);
|
|
3293
|
+
handleProfilerCpuStop = (args) => this.perf.handleProfilerCpuStop(args);
|
|
3294
|
+
handleProfilerCpuDispatch = (args) => String(args["action"] ?? "") === "stop" ? this.perf.handleProfilerCpuStop(args) : this.perf.handleProfilerCpuStart(args);
|
|
3295
|
+
handleProfilerHeapSamplingStart = (args) => this.perf.handleProfilerHeapSamplingStart(args);
|
|
3296
|
+
handleProfilerHeapSamplingStop = (args) => this.perf.handleProfilerHeapSamplingStop(args);
|
|
3297
|
+
handleProfilerHeapSamplingDispatch = (args) => String(args["action"] ?? "") === "stop" ? this.perf.handleProfilerHeapSamplingStop(args) : this.perf.handleProfilerHeapSamplingStart(args);
|
|
3298
|
+
handleConsoleGetExceptions = (args) => this.console_.handleConsoleGetExceptions(args);
|
|
3299
|
+
handleConsoleInjectDispatch = (args) => {
|
|
3300
|
+
switch (String(args["type"] ?? "")) {
|
|
3301
|
+
case "xhr": return this.console_.handleConsoleInjectXhrInterceptor(args);
|
|
3302
|
+
case "fetch": return this.console_.handleConsoleInjectFetchInterceptor(args);
|
|
3303
|
+
case "function": return this.console_.handleConsoleInjectFunctionTracer(args);
|
|
3304
|
+
default: return this.console_.handleConsoleInjectScriptMonitor(args);
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
handleConsoleBuffersDispatch = (args) => {
|
|
3308
|
+
return String(args["action"] ?? "") === "reset" ? this.console_.handleConsoleResetInjectedInterceptors(args) : this.console_.handleConsoleClearInjectedBuffers(args);
|
|
3309
|
+
};
|
|
3310
|
+
handleConsoleInjectScriptMonitor = (args) => this.console_.handleConsoleInjectScriptMonitor(args);
|
|
3311
|
+
handleConsoleInjectXhrInterceptor = (args) => this.console_.handleConsoleInjectXhrInterceptor(args);
|
|
3312
|
+
handleConsoleInjectFetchInterceptor = (args) => this.console_.handleConsoleInjectFetchInterceptor(args);
|
|
3313
|
+
handleConsoleClearInjectedBuffers = (args) => this.console_.handleConsoleClearInjectedBuffers(args);
|
|
3314
|
+
handleConsoleResetInjectedInterceptors = (args) => this.console_.handleConsoleResetInjectedInterceptors(args);
|
|
3315
|
+
handleConsoleInjectFunctionTracer = (args) => this.console_.handleConsoleInjectFunctionTracer(args);
|
|
3316
|
+
handleNetworkExtractAuth = (args) => this.replay.handleNetworkExtractAuth(args);
|
|
3317
|
+
handleNetworkExportHar = (args) => this.replay.handleNetworkExportHar(args);
|
|
3318
|
+
handleNetworkReplayRequest = (args) => this.replay.handleNetworkReplayRequest(args);
|
|
3319
|
+
handleNetworkInterceptResponse = (args) => this.intercept.handleNetworkInterceptResponse(args);
|
|
3320
|
+
handleNetworkInterceptList = (args) => this.intercept.handleNetworkInterceptList(args);
|
|
3321
|
+
handleNetworkInterceptDisable = (args) => this.intercept.handleNetworkInterceptDisable(args);
|
|
3322
|
+
handleNetworkInterceptDispatch = (args) => {
|
|
3323
|
+
const action = String(args["action"] ?? "");
|
|
3324
|
+
switch (action) {
|
|
3325
|
+
case "add": return this.intercept.handleNetworkInterceptResponse(args);
|
|
3326
|
+
case "list": return this.intercept.handleNetworkInterceptList(args);
|
|
3327
|
+
case "disable": return this.intercept.handleNetworkInterceptDisable(args);
|
|
3328
|
+
default: return Promise.resolve({
|
|
3329
|
+
content: [{
|
|
3330
|
+
type: "text",
|
|
3331
|
+
text: `Invalid action: "${action}". Expected one of: add, list, disable`
|
|
3332
|
+
}],
|
|
3333
|
+
isError: true
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
handleNetworkTraceroute = (args) => this.raw.handleNetworkTraceroute(args);
|
|
3338
|
+
handleNetworkIcmpProbe = (args) => this.raw.handleNetworkIcmpProbe(args);
|
|
3339
|
+
handleHttpRequestBuild = (args) => this.raw.handleHttpRequestBuild(args);
|
|
3340
|
+
handleHttpPlainRequest = (args) => this.raw.handleHttpPlainRequest(args);
|
|
3341
|
+
handleHttp2Probe = (args) => this.raw.handleHttp2Probe(args);
|
|
3342
|
+
handleHttp2FrameBuild = (args) => this.raw.handleHttp2FrameBuild(args);
|
|
3343
|
+
handleNetworkRttMeasure = (args) => this.raw.handleNetworkRttMeasure(args);
|
|
3344
|
+
};
|
|
3345
|
+
//#endregion
|
|
3346
|
+
export { AdvancedToolHandlers };
|