@jshookmcp/jshook 0.2.9 → 0.3.0
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 +2 -2
- package/README.zh.md +2 -2
- package/dist/{AntiCheatDetector-BNk-EoBt.mjs → AntiCheatDetector-CqGDXmfc.mjs} +159 -53
- package/dist/{CodeInjector-Cq8q01kp.mjs → CodeInjector-BdjRfNx7.mjs} +5 -5
- package/dist/{ConsoleMonitor-CPVQW1Y-.mjs → ConsoleMonitor-DykL3IAw.mjs} +85 -17
- package/dist/{DetailedDataManager-BQQcxh64.mjs → DetailedDataManager-HT49OrvF.mjs} +1 -1
- package/dist/{ExtensionManager-CWYgw0YW.mjs → ExtensionManager-BDMsY2Dz.mjs} +15 -8
- package/dist/{HardwareBreakpoint-B9gZCdFP.mjs → HardwareBreakpoint-Cc2AFq1Y.mjs} +3 -3
- package/dist/{HeapAnalyzer-BLDH0dCv.mjs → HeapAnalyzer-DruMgsgj.mjs} +20 -20
- package/dist/{HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs → HookGeneratorBuilders.core.generators.storage-CTbB4Lcx.mjs} +1 -74
- package/dist/{InstrumentationSession-CvPC7Jwy.mjs → InstrumentationSession-DLH0vd-z.mjs} +2 -2
- package/dist/{MemoryController-CbVdCIJF.mjs → MemoryController-CMtviNW_.mjs} +3 -3
- package/dist/{MemoryScanSession-BsDZbLYm.mjs → MemoryScanSession-ITgb_NMi.mjs} +2 -2
- package/dist/{MemoryScanner-Bcpml6II.mjs → MemoryScanner-CiL7Z3ey.mjs} +12 -9
- package/dist/{NativeMemoryManager.impl-dZtA1ZGn.mjs → NativeMemoryManager.impl-D9Lkovvn.mjs} +13 -10
- package/dist/{NativeMemoryManager.utils-B-FjA2mJ.mjs → NativeMemoryManager.utils-BBlAixF5.mjs} +1 -1
- package/dist/{PEAnalyzer-D1lzJ_VG.mjs → PEAnalyzer-DMQ44gen.mjs} +15 -15
- package/dist/{PageController-Bqm2kZ_X.mjs → PageController-BPJNqqBN.mjs} +18 -4
- package/dist/{PointerChainEngine-BOhyVsjx.mjs → PointerChainEngine-K7wN8Z-w.mjs} +10 -7
- package/dist/ProcessRegistry-zGg12QbE.mjs +74 -0
- package/dist/{ResponseBuilder-D3iFYx2N.mjs → ResponseBuilder-CJXWmWNw.mjs} +10 -10
- package/dist/{ScriptManager-aHHq0X7U.mjs → ScriptManager-ZuWD-0Jg.mjs} +195 -192
- package/dist/{Speedhack-CqdIFlQl.mjs → Speedhack-D-z0umeT.mjs} +2 -2
- package/dist/{StructureAnalyzer-DhFaPvRO.mjs → StructureAnalyzer-Cav5AVSL.mjs} +9 -6
- package/dist/{ToolCatalog-C0JGZoOm.mjs → ToolCatalog-5OJdMiF0.mjs} +81 -81
- package/dist/{ToolProbe-oC7aPrkv.mjs → ToolProbe-DbCFGyrg.mjs} +1 -1
- package/dist/{ToolRegistry-BjaF4oNz.mjs → ToolRegistry-B9krbTtI.mjs} +51 -2
- package/dist/{ToolRouter.policy-BWV67ZK-.mjs → ToolRouter.policy-BGDAGyeH.mjs} +60 -20
- package/dist/TraceRecorder-B41Z5XBj.mjs +1286 -0
- package/dist/{Win32API-CePkipZY.mjs → Win32API-C2kjj0ze.mjs} +18 -12
- package/dist/{Win32Debug-BvKs-gxc.mjs → Win32Debug-CKrGOTpo.mjs} +2 -2
- package/dist/{WorkflowEngine-CuvkZtWu.mjs → WorkflowEngine-DJ6M4opp.mjs} +226 -255
- package/dist/analysis-BHeJW2Nb.mjs +1234 -0
- package/dist/{antidebug-CqDTB_uk.mjs → antidebug-BRKeyt27.mjs} +3 -3
- package/dist/{artifactRetention-CFEprwPw.mjs → artifactRetention-CPXkUJXp.mjs} +13 -6
- package/dist/{artifacts-Bk2-_uPq.mjs → artifacts-DkfosXH3.mjs} +1 -1
- package/dist/authorization-schema-DRqyJMSk.mjs +31 -0
- package/dist/{binary-instrument-CXfpx6fT.mjs → binary-instrument--V3MAhJ4.mjs} +19 -27
- package/dist/bind-helpers-ClV34xdn.mjs +42 -0
- package/dist/{boringssl-inspector-BH2D3VKc.mjs → boringssl-inspector-Bo_LOLaS.mjs} +1 -1
- package/dist/{browser-BpOr5PEx.mjs → browser-Dx3_S2cG.mjs} +324 -37
- package/dist/capabilities-CcHlvWgK.mjs +33 -0
- package/dist/{constants-B0OANIBL.mjs → constants-CDZLOoVv.mjs} +18 -3
- package/dist/{coordination-qUbyF8KU.mjs → coordination-DgItD9DL.mjs} +2 -2
- package/dist/{debugger-gnKxRSN0.mjs → debugger-RS3RSAqs.mjs} +30 -13
- package/dist/definitions-BEoYofW5.mjs +47 -0
- package/dist/{definitions-bAhHQJq9.mjs → definitions-BRaefg3u.mjs} +11 -5
- package/dist/{definitions-DVGfrn7y.mjs → definitions-BbkvZkiv.mjs} +2 -2
- package/dist/definitions-BtWSHJ3o.mjs +17 -0
- package/dist/{definitions-BMfYXoNC.mjs → definitions-C1gCHO0i.mjs} +1 -1
- package/dist/{definitions-C1UvM5Iy.mjs → definitions-CDOg_b-l.mjs} +14 -2
- package/dist/definitions-CVPD9hzZ.mjs +54 -0
- package/dist/{definitions-Cke7zEb8.mjs → definitions-Cea8Lgl7.mjs} +1 -1
- package/dist/definitions-DAgIyjxM.mjs +10 -0
- package/dist/{definitions-B4rAvHNZ.mjs → definitions-DJA27nsL.mjs} +12 -9
- package/dist/{definitions-ClJLzsJQ.mjs → definitions-DKPFU3LW.mjs} +1 -1
- package/dist/{definitions-D3VsGcvz.mjs → definitions-DPRpZQ96.mjs} +7 -7
- package/dist/{definitions-B18eyf0B.mjs → definitions-DUE5gmdn.mjs} +1 -1
- package/dist/definitions-DYVjOtxa.mjs +26 -0
- package/dist/{definitions-BB_4jnmy.mjs → definitions-DcYLVLCo.mjs} +1 -1
- package/dist/{definitions-Beid2EB3.mjs → definitions-Pp5LI2H4.mjs} +1 -1
- package/dist/definitions-j9KdHVNR.mjs +14 -0
- package/dist/definitions-uzkjBwa7.mjs +258 -0
- package/dist/{definitions-Cq-zroAU.mjs → definitions-va-AnLuQ.mjs} +4 -4
- package/dist/{encoding-Bvz5jLRv.mjs → encoding-DJeqHmpd.mjs} +18 -4
- package/dist/{evidence-graph-bridge-C_fv9PuC.mjs → evidence-graph-bridge-DcYizFk2.mjs} +1 -0
- package/dist/{factory-DxlGh9Xf.mjs → factory-C90tBff6.mjs} +6 -6
- package/dist/flat-target-session-Dgax2Cy3.mjs +29 -0
- package/dist/{graphql-DYWzJ29s.mjs → graphql-CoHrhweh.mjs} +205 -34
- package/dist/{handlers-C67ktuRN.mjs → handlers-4jmR0nMs.mjs} +220 -32
- package/dist/{handlers-DlCJN4Td.mjs → handlers-BAHPxcch.mjs} +122 -90
- package/dist/{handlers-9sAbfIg-.mjs → handlers-BOs9b907.mjs} +849 -801
- package/dist/{handlers-DxGIq15_2.mjs → handlers-BWXEy6ef.mjs} +16 -16
- package/dist/{handlers-tB9Mp9ZK.mjs → handlers-Bndn6QvE.mjs} +31 -4
- package/dist/{handlers-CTsDAO6p.mjs → handlers-BqC4bD4s.mjs} +1 -1
- package/dist/{handlers-C87g8oCe.mjs → handlers-BtYq60bM2.mjs} +1 -1
- package/dist/{handlers-DeLOCd5m.mjs → handlers-BzgcB4iv.mjs} +17 -17
- package/dist/{handlers-Cgyg6c0U.mjs → handlers-CRyRWj2b.mjs} +237 -23
- package/dist/{handlers-U6L4xhuF.mjs → handlers-CVv2H1uq.mjs} +24 -17
- package/dist/{handlers-tiy7EIBp.mjs → handlers-Dl5a7JS4.mjs} +3 -3
- package/dist/{handlers-D6j6yka7.mjs → handlers-Dx2d7jt7.mjs} +1893 -1480
- package/dist/{handlers-Bl8zkwz1.mjs → handlers-Dz9PYsCa.mjs} +95 -6
- package/dist/handlers-HujRKC3b.mjs +661 -0
- package/dist/{handlers.impl-DS0d9fUw.mjs → handlers.impl-XWXkQfyi.mjs} +70 -24
- package/dist/{hooks-CzCWByww.mjs → hooks-B1B8NRHL.mjs} +3 -3
- package/dist/index.mjs +154 -144
- package/dist/{maintenance-P7ePRXQC.mjs → maintenance-PRMkLVRW.mjs} +35 -30
- package/dist/manifest-67Bok-Si.mjs +58 -0
- package/dist/{manifest-B3QVVeBS.mjs → manifest-6lNTMZAB2.mjs} +33 -28
- package/dist/manifest-B2duEHiH.mjs +90 -0
- package/dist/manifest-B6EY9Vm8.mjs +57 -0
- package/dist/{manifest-gZ4s_UtG.mjs → manifest-B6nKSbyY.mjs} +32 -33
- package/dist/{manifest-2ToTpjv8.mjs → manifest-BL8AQNPF.mjs} +31 -31
- package/dist/{manifest-DzwvxPJX.mjs → manifest-BSZvJJmV.mjs} +23 -14
- package/dist/{manifest-Sc_0JQ13.mjs → manifest-BU7qzUyX.mjs} +23 -23
- package/dist/{manifest-CT7zZBV1.mjs → manifest-Bl62e8WK.mjs} +24 -23
- package/dist/manifest-Bo5cXjdt.mjs +82 -0
- package/dist/manifest-BpS4gtUK.mjs +1347 -0
- package/dist/manifest-Bv65_e2W.mjs +101 -0
- package/dist/manifest-BytNIF4Z.mjs +117 -0
- package/dist/{manifest-BqrQ4Tpj.mjs → manifest-C-xtsjS3.mjs} +23 -23
- package/dist/{manifest-NXctwWQq.mjs → manifest-CDYl7OhA.mjs} +36 -38
- package/dist/manifest-CRZ3xmkD.mjs +61 -0
- package/dist/manifest-CoW6u4Tp.mjs +132 -0
- package/dist/manifest-Cq5zN_8A.mjs +50 -0
- package/dist/{manifest-CAhOuvSl.mjs → manifest-D7YZM_2e.mjs} +75 -85
- package/dist/{manifest-DCyjf4n2.mjs → manifest-DE_VrAeQ.mjs} +27 -7
- package/dist/manifest-DGsXSCpT.mjs +39 -0
- package/dist/{manifest-BB2J8IMJ.mjs → manifest-DJ2vfEuW.mjs} +48 -41
- package/dist/{manifest-3g71z6Bg.mjs → manifest-DPXDYhEu.mjs} +26 -25
- package/dist/manifest-Dd4fQb0a.mjs +322 -0
- package/dist/{manifest-CXsRWjjI.mjs → manifest-Deq6opGg.mjs} +95 -96
- package/dist/{manifest-C9RT5nk32.mjs → manifest-DfJTafJK.mjs} +14 -11
- package/dist/manifest-DgOdgN_j.mjs +50 -0
- package/dist/{manifest-BmtZzQiQ2.mjs → manifest-DlbMW4v4.mjs} +17 -15
- package/dist/{manifest-DrbmZcFl2.mjs → manifest-DmVfbH0w.mjs} +212 -91
- package/dist/manifest-Dog6Ddjr.mjs +109 -0
- package/dist/manifest-DvgU5FWb.mjs +58 -0
- package/dist/manifest-HsfDBs7j.mjs +50 -0
- package/dist/manifest-I8oQHvCG.mjs +186 -0
- package/dist/manifest-NvH_a-av.mjs +786 -0
- package/dist/{manifest-Dh8WBmEW.mjs → manifest-cEJU1v0Z.mjs} +24 -24
- package/dist/manifest-wOl5XLB12.mjs +112 -0
- package/dist/{modules-C184v-S9.mjs → modules-tZozf0LQ.mjs} +130 -860
- package/dist/{mojo-ipc-B_H61Afw.mjs → mojo-ipc-DXNEXEqb.mjs} +141 -26
- package/dist/{network-671Cw6hV.mjs → network-CPVvwvFg.mjs} +1329 -823
- package/dist/{outputPaths-B1uGmrWZ.mjs → outputPaths-um7lCRY3.mjs} +4 -8
- package/dist/{platform-WmNn8Sxb.mjs → platform-CYeFoTWp.mjs} +101 -10
- package/dist/{process-QcbIy5Zq.mjs → process-BTbgcVc6.mjs} +251 -346
- package/dist/{proxy-DqNs0bAd.mjs → proxy-r8YN6nP1.mjs} +30 -8
- package/dist/{registry-D-6e18lB.mjs → registry-Bl8ZQW61.mjs} +3 -3
- package/dist/{response-BQVP-xUn.mjs → response-CWhh2aLo.mjs} +7 -1
- package/dist/{shared-state-board-DV-dpHFJ.mjs → shared-state-board-BoZnSoj-.mjs} +2 -2
- package/dist/{sourcemap-Dq8ez8vS.mjs → sourcemap-BIDHUVXy.mjs} +350 -66
- package/dist/{streaming-BUQ0VJsg.mjs → streaming-Dal6utPp.mjs} +13 -13
- package/dist/{tool-builder-DCbIC5Eo.mjs → tool-builder-BHJp32mV.mjs} +1 -1
- package/dist/{transform-CiYJfNX0.mjs → transform-DRVgGG90.mjs} +18 -14
- package/dist/wasm-BYx5UOeG.mjs +1044 -0
- package/dist/webcrack-Be0_FccV.mjs +747 -0
- package/dist/{workflow-f3xJOcjx.mjs → workflow-BpuKEtvn.mjs} +8 -8
- package/package.json +76 -43
- package/dist/TraceRecorder-DgxyVbdQ.mjs +0 -519
- package/dist/analysis-CL9uACt9.mjs +0 -463
- package/dist/bind-helpers-xFfRF-qm.mjs +0 -22
- package/dist/definitions-6M-eejaT.mjs +0 -53
- package/dist/definitions-B3QdlrHv.mjs +0 -34
- package/dist/definitions-CXEI7QC72.mjs +0 -216
- package/dist/definitions-C_4r7Fo-2.mjs +0 -14
- package/dist/definitions-CkFDALoa.mjs +0 -26
- package/dist/definitions-Cy3Sl6gV.mjs +0 -34
- package/dist/definitions-LKpC3-nL.mjs +0 -9
- package/dist/handlers-DdFzXLvF.mjs +0 -446
- package/dist/manifest-82baTv4U.mjs +0 -45
- package/dist/manifest-BKbgbSiY.mjs +0 -60
- package/dist/manifest-Bcf-TJzH.mjs +0 -848
- package/dist/manifest-Bnd7kqEY.mjs +0 -55
- package/dist/manifest-BqQX6OQC2.mjs +0 -65
- package/dist/manifest-Br4RPFt5.mjs +0 -370
- package/dist/manifest-C5qDjysN.mjs +0 -107
- package/dist/manifest-CBYWCUBJ.mjs +0 -51
- package/dist/manifest-CFADCRa1.mjs +0 -37
- package/dist/manifest-CQVhavRF.mjs +0 -114
- package/dist/manifest-CV12bcrF.mjs +0 -121
- package/dist/manifest-CZLUCfG02.mjs +0 -95
- package/dist/manifest-D6phHKFd.mjs +0 -131
- package/dist/manifest-DHsnKgP6.mjs +0 -60
- package/dist/manifest-Df_dliIe.mjs +0 -55
- package/dist/manifest-DhKRAT8_.mjs +0 -92
- package/dist/manifest-DlpTj4ic2.mjs +0 -193
- package/dist/manifest-DuwHjUa5.mjs +0 -70
- package/dist/manifest-qSleDqdO.mjs +0 -1023
- package/dist/wasm-DQTnHDs4.mjs +0 -531
- /package/dist/{CacheAdapters-CDe5WPSV.mjs → CacheAdapters-jJFy20G-.mjs} +0 -0
- /package/dist/{DarwinAPI-BNPxu0RH.mjs → DarwinAPI-ETyy0xyo.mjs} +0 -0
- /package/dist/{EventBus-DgPmwpeu.mjs → EventBus-DFKvADm3.mjs} +0 -0
- /package/dist/{EvidenceGraphBridge-SFesNera.mjs → EvidenceGraphBridge-318Oi0Lf.mjs} +0 -0
- /package/dist/{FingerprintManager-gzWtkKuf.mjs → FingerprintManager-BN4UQWnX.mjs} +0 -0
- /package/dist/{PrerequisiteError-Dl33Svkz.mjs → PrerequisiteError-TuyZIs6n.mjs} +0 -0
- /package/dist/{ReverseEvidenceGraph-Dlsk94LC.mjs → ReverseEvidenceGraph-C02-gXOh.mjs} +0 -0
- /package/dist/{StealthVerifier-Bo4T3bz8.mjs → StealthVerifier-BWmPgQsv.mjs} +0 -0
- /package/dist/{VersionDetector-CwVLVdDM.mjs → VersionDetector-K3V4vGsw.mjs} +0 -0
- /package/dist/{betterSqlite3-0pqusHHH.mjs → betterSqlite3-DLSBZodi.mjs} +0 -0
- /package/dist/{concurrency-Bt0yv1kJ.mjs → concurrency-Drev_Vz9.mjs} +0 -0
- /package/dist/{formatAddress-DVkj9kpI.mjs → formatAddress-nnMvEohD.mjs} +0 -0
- /package/dist/{parse-args-BlRjqlkL.mjs → parse-args-B4cY5Vx5.mjs} +0 -0
- /package/dist/{ssrf-policy-ZaUfvhq7.mjs → ssrf-policy-Dsqd-DTX.mjs} +0 -0
- /package/dist/{types-CPhOReNX.mjs → types-DDBWs9UP.mjs} +0 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { t as logger } from "./logger-Dh_xb7_2.mjs";
|
|
2
|
-
import { Gt as
|
|
3
|
-
import { t as DetailedDataManager } from "./DetailedDataManager-
|
|
4
|
-
import { a as PerformanceMonitor } from "./modules-
|
|
5
|
-
import { n as argEnum, t as argBool } from "./parse-args-
|
|
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-
|
|
7
|
-
import { t as R } from "./ResponseBuilder-
|
|
8
|
-
import "./definitions-
|
|
2
|
+
import { Gt as NETWORK_HAR_BODY_CONCURRENCY, Kt as NETWORK_REPLAY_MAX_REDIRECTS, ft as ICMP_DEFAULT_PACKET_SIZE, g as BOT_DETECT_LIMIT_DEFAULT, mt as ICMP_TRACEROUTE_MAX_HOPS, pt as ICMP_PROBE_TIMEOUT_MS } from "./constants-CDZLOoVv.mjs";
|
|
3
|
+
import { t as DetailedDataManager } from "./DetailedDataManager-HT49OrvF.mjs";
|
|
4
|
+
import { a as PerformanceMonitor } from "./modules-tZozf0LQ.mjs";
|
|
5
|
+
import { n as argEnum, t as argBool } from "./parse-args-B4cY5Vx5.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-Dsqd-DTX.mjs";
|
|
7
|
+
import { t as R } from "./ResponseBuilder-CJXWmWNw.mjs";
|
|
8
|
+
import "./definitions-uzkjBwa7.mjs";
|
|
9
9
|
import * as http$1 from "node:http";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import { promises } from "node:fs";
|
|
11
12
|
import koffi from "koffi";
|
|
12
13
|
import * as net from "node:net";
|
|
13
|
-
import * as tls from "node:tls";
|
|
14
14
|
import * as dns from "node:dns/promises";
|
|
15
|
-
import * as
|
|
15
|
+
import * as tls from "node:tls";
|
|
16
16
|
import * as http2 from "node:http2";
|
|
17
|
+
import * as https from "node:https";
|
|
17
18
|
//#region src/server/domains/network/handlers.base.types.ts
|
|
18
19
|
/** Resource types excluded by default when no explicit filters are set. */
|
|
19
20
|
const EXCLUDED_RESOURCE_TYPES = new Set([
|
|
@@ -564,9 +565,27 @@ var PerformanceHandlers = class {
|
|
|
564
565
|
}
|
|
565
566
|
async handlePerformanceTakeHeapSnapshot(_args) {
|
|
566
567
|
try {
|
|
568
|
+
const traceRecorder = this.deps.getTraceRecorder?.() ?? null;
|
|
569
|
+
if (traceRecorder?.getState() === "recording") try {
|
|
570
|
+
const snapshotSize = await traceRecorder.captureActiveHeapSnapshot();
|
|
571
|
+
return R.ok().merge({
|
|
572
|
+
snapshotSize,
|
|
573
|
+
persistedToTrace: true,
|
|
574
|
+
message: "Heap snapshot taken and persisted to the active trace recording (data too large to return)"
|
|
575
|
+
}).json();
|
|
576
|
+
} catch (traceError) {
|
|
577
|
+
const snapshotSize = await this.deps.getPerformanceMonitor().takeHeapSnapshot();
|
|
578
|
+
return R.ok().merge({
|
|
579
|
+
snapshotSize,
|
|
580
|
+
persistedToTrace: false,
|
|
581
|
+
tracePersistenceError: traceError instanceof Error ? traceError.message : String(traceError),
|
|
582
|
+
message: "Heap snapshot taken, but the active trace recording could not persist it (data too large to return)"
|
|
583
|
+
}).json();
|
|
584
|
+
}
|
|
567
585
|
const snapshotSize = await this.deps.getPerformanceMonitor().takeHeapSnapshot();
|
|
568
586
|
return R.ok().merge({
|
|
569
587
|
snapshotSize,
|
|
588
|
+
persistedToTrace: false,
|
|
570
589
|
message: "Heap snapshot taken (data too large to return, saved internally)"
|
|
571
590
|
}).json();
|
|
572
591
|
} catch (error) {
|
|
@@ -582,7 +601,7 @@ var PerformanceHandlers = class {
|
|
|
582
601
|
categories,
|
|
583
602
|
screenshots
|
|
584
603
|
});
|
|
585
|
-
return R.ok().set("message", "Performance tracing started. Call
|
|
604
|
+
return R.ok().set("message", "Performance tracing started. Call performance_trace with action=\"stop\" to save the trace.").json();
|
|
586
605
|
} catch (error) {
|
|
587
606
|
return R.fail(error).json();
|
|
588
607
|
}
|
|
@@ -606,7 +625,7 @@ var PerformanceHandlers = class {
|
|
|
606
625
|
async handleProfilerCpuStart(_args) {
|
|
607
626
|
try {
|
|
608
627
|
await this.deps.getPerformanceMonitor().startCPUProfiling();
|
|
609
|
-
return R.ok().set("message", "CPU profiling started. Call
|
|
628
|
+
return R.ok().set("message", "CPU profiling started. Call profiler_cpu with action=\"stop\" to save the profile.").json();
|
|
610
629
|
} catch (error) {
|
|
611
630
|
return R.fail(error).json();
|
|
612
631
|
}
|
|
@@ -616,7 +635,7 @@ var PerformanceHandlers = class {
|
|
|
616
635
|
const profileRaw = await this.deps.getPerformanceMonitor().stopCPUProfiling();
|
|
617
636
|
const profile = toCpuProfilePayload(profileRaw) || profileRaw;
|
|
618
637
|
const { writeFile } = await import("node:fs/promises");
|
|
619
|
-
const { resolveArtifactPath } = await import("./artifacts-
|
|
638
|
+
const { resolveArtifactPath } = await import("./artifacts-DkfosXH3.mjs").then((n) => n.t);
|
|
620
639
|
const artifactPath = asOptionalString(args.artifactPath);
|
|
621
640
|
const profileJson = JSON.stringify(profile, null, 2);
|
|
622
641
|
let savedPath;
|
|
@@ -655,7 +674,7 @@ var PerformanceHandlers = class {
|
|
|
655
674
|
const monitor = this.deps.getPerformanceMonitor();
|
|
656
675
|
const samplingInterval = asOptionalNumber(args.samplingInterval);
|
|
657
676
|
await monitor.startHeapSampling({ samplingInterval });
|
|
658
|
-
return R.ok().set("message", "Heap sampling started. Call
|
|
677
|
+
return R.ok().set("message", "Heap sampling started. Call profiler_heap_sampling with action=\"stop\" to save the report.").json();
|
|
659
678
|
} catch (error) {
|
|
660
679
|
return R.fail(error).json();
|
|
661
680
|
}
|
|
@@ -1030,6 +1049,14 @@ const DANGEROUS_KEYS = new Set([
|
|
|
1030
1049
|
"constructor",
|
|
1031
1050
|
"prototype"
|
|
1032
1051
|
]);
|
|
1052
|
+
function buildCookieHeader(profile) {
|
|
1053
|
+
const parts = [];
|
|
1054
|
+
for (const cookie of profile.cookies) {
|
|
1055
|
+
if (!cookie.name) continue;
|
|
1056
|
+
parts.push(`${cookie.name}=${cookie.value}`);
|
|
1057
|
+
}
|
|
1058
|
+
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
1059
|
+
}
|
|
1033
1060
|
function sanitizeHeaders(headers) {
|
|
1034
1061
|
const out = Object.create(null);
|
|
1035
1062
|
for (const [k, v] of Object.entries(headers)) if (!STRIPPED_HEADERS.has(k.toLowerCase()) && !DANGEROUS_KEYS.has(k)) out[k] = v;
|
|
@@ -1042,6 +1069,11 @@ async function replayRequest(base, args, maxBodyBytes = 512e3) {
|
|
|
1042
1069
|
...base.headers,
|
|
1043
1070
|
...args.headerPatch
|
|
1044
1071
|
});
|
|
1072
|
+
const cookieHeader = args.sessionProfile ? buildCookieHeader(args.sessionProfile) : void 0;
|
|
1073
|
+
if (cookieHeader) mergedHeaders.Cookie = cookieHeader;
|
|
1074
|
+
if (args.sessionProfile?.userAgent && !mergedHeaders["User-Agent"] && !mergedHeaders["user-agent"]) mergedHeaders["User-Agent"] = args.sessionProfile.userAgent;
|
|
1075
|
+
if (args.sessionProfile?.acceptLanguage && !mergedHeaders["Accept-Language"] && !mergedHeaders["accept-language"]) mergedHeaders["Accept-Language"] = args.sessionProfile.acceptLanguage;
|
|
1076
|
+
if (args.sessionProfile?.referer && !mergedHeaders["Referer"] && !mergedHeaders["referer"]) mergedHeaders.Referer = args.sessionProfile.referer;
|
|
1045
1077
|
const body = args.bodyPatch !== void 0 ? args.bodyPatch : base.postData;
|
|
1046
1078
|
const authorizationPolicy = createNetworkAuthorizationPolicy(args.authorization);
|
|
1047
1079
|
const allowLegacyLocalSsrf = !authorizationPolicy && isLocalSsrfBypassEnabled();
|
|
@@ -1310,6 +1342,7 @@ var ReplayHandlers = class {
|
|
|
1310
1342
|
const result = await replayRequest(base, {
|
|
1311
1343
|
requestId,
|
|
1312
1344
|
headerPatch: args.headerPatch,
|
|
1345
|
+
sessionProfile: args.sessionProfile,
|
|
1313
1346
|
bodyPatch: args.bodyPatch,
|
|
1314
1347
|
methodOverride: args.methodOverride,
|
|
1315
1348
|
urlOverride: args.urlOverride,
|
|
@@ -2164,755 +2197,205 @@ function performHttp2ProbeInternal(options) {
|
|
|
2164
2197
|
});
|
|
2165
2198
|
}
|
|
2166
2199
|
//#endregion
|
|
2167
|
-
//#region src/
|
|
2168
|
-
|
|
2169
|
-
|
|
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");
|
|
2200
|
+
//#region src/server/domains/network/handlers/raw-dns-http-handlers.ts
|
|
2201
|
+
var RawDnsHttpHandlers = class {
|
|
2202
|
+
constructor(eventBus) {
|
|
2203
|
+
this.eventBus = eventBus;
|
|
2230
2204
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
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);
|
|
2205
|
+
async handleDnsResolve(args) {
|
|
2206
|
+
try {
|
|
2207
|
+
const hostname = parseOptionalString(args.hostname, "hostname");
|
|
2208
|
+
if (!hostname) return R.text("hostname is required", true);
|
|
2209
|
+
const rrType = parseOptionalString(args.rrType, "rrType") ?? "A";
|
|
2210
|
+
const validTypes = [
|
|
2211
|
+
"A",
|
|
2212
|
+
"AAAA",
|
|
2213
|
+
"MX",
|
|
2214
|
+
"TXT",
|
|
2215
|
+
"NS",
|
|
2216
|
+
"CNAME",
|
|
2217
|
+
"SOA",
|
|
2218
|
+
"PTR",
|
|
2219
|
+
"SRV",
|
|
2220
|
+
"ANY"
|
|
2221
|
+
];
|
|
2222
|
+
if (!validTypes.includes(rrType)) return R.text(`Invalid rrType: "${rrType}". Expected one of: ${validTypes.join(", ")}`, true);
|
|
2223
|
+
const start = performance.now();
|
|
2224
|
+
const records = await dns.resolve(hostname, rrType);
|
|
2225
|
+
const timing = Math.round((performance.now() - start) * 100) / 100;
|
|
2226
|
+
return R.ok().json({
|
|
2227
|
+
hostname,
|
|
2228
|
+
rrType,
|
|
2229
|
+
records,
|
|
2230
|
+
timing
|
|
2231
|
+
});
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2234
|
+
return R.fail(`DNS resolve failed: ${message}`).json();
|
|
2235
|
+
}
|
|
2308
2236
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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)
|
|
2237
|
+
async handleDnsReverse(args) {
|
|
2238
|
+
try {
|
|
2239
|
+
const ip = parseOptionalString(args.ip, "ip");
|
|
2240
|
+
if (!ip) return R.text("ip is required", true);
|
|
2241
|
+
const start = performance.now();
|
|
2242
|
+
const hostnames = await dns.reverse(ip);
|
|
2243
|
+
const timing = Math.round((performance.now() - start) * 100) / 100;
|
|
2244
|
+
return R.ok().json({
|
|
2245
|
+
ip,
|
|
2246
|
+
hostnames,
|
|
2247
|
+
timing
|
|
2345
2248
|
});
|
|
2346
|
-
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2251
|
+
return R.fail(`DNS reverse lookup failed: ${message}`).json();
|
|
2347
2252
|
}
|
|
2348
|
-
} finally {
|
|
2349
|
-
win_IcmpCloseHandle(handle);
|
|
2350
2253
|
}
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2254
|
+
async handleHttpRequestBuild(args) {
|
|
2255
|
+
try {
|
|
2256
|
+
const method = parseOptionalString(args.method, "method");
|
|
2257
|
+
const target = parseOptionalString(args.target, "target");
|
|
2258
|
+
if (!method) throw new Error("method is required");
|
|
2259
|
+
if (!target) throw new Error("target is required");
|
|
2260
|
+
const built = buildHttpRequest({
|
|
2261
|
+
method,
|
|
2262
|
+
target,
|
|
2263
|
+
host: parseOptionalString(args.host, "host"),
|
|
2264
|
+
headers: parseHeaderRecord(args.headers, "headers"),
|
|
2265
|
+
body: parseRawString(args.body, "body", { allowEmpty: true }),
|
|
2266
|
+
httpVersion: parseOptionalString(args.httpVersion, "httpVersion") ?? "1.1",
|
|
2267
|
+
addHostHeader: parseBooleanArg(args.addHostHeader, true),
|
|
2268
|
+
addContentLength: parseBooleanArg(args.addContentLength, true),
|
|
2269
|
+
addConnectionClose: parseBooleanArg(args.addConnectionClose, true)
|
|
2270
|
+
});
|
|
2271
|
+
emitEvent(this.eventBus, "network:http_request_built", {
|
|
2272
|
+
method: built.startLine.split(" ", 1)[0] ?? "UNKNOWN",
|
|
2273
|
+
target,
|
|
2274
|
+
byteLength: built.requestBytes,
|
|
2275
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2276
|
+
});
|
|
2277
|
+
return R.ok().merge(built).json();
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
2280
|
+
}
|
|
2374
2281
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2282
|
+
async handleHttpPlainRequest(args) {
|
|
2283
|
+
try {
|
|
2284
|
+
const hostArg = parseOptionalString(args.host, "host");
|
|
2285
|
+
const requestText = parseRawString(args.requestText, "requestText");
|
|
2286
|
+
if (!hostArg) throw new Error("host is required");
|
|
2287
|
+
if (!requestText) throw new Error("requestText is required");
|
|
2288
|
+
const host = normalizeTargetHost(hostArg);
|
|
2289
|
+
const port = parseNumberArg(args.port, {
|
|
2290
|
+
defaultValue: 80,
|
|
2291
|
+
min: 1,
|
|
2292
|
+
max: 65535,
|
|
2293
|
+
integer: true
|
|
2294
|
+
});
|
|
2295
|
+
const timeoutMs = parseNumberArg(args.timeoutMs, {
|
|
2296
|
+
defaultValue: 3e4,
|
|
2297
|
+
min: 1,
|
|
2298
|
+
max: 12e4,
|
|
2299
|
+
integer: true
|
|
2300
|
+
});
|
|
2301
|
+
const maxResponseBytes = parseNumberArg(args.maxResponseBytes, {
|
|
2302
|
+
defaultValue: 512e3,
|
|
2303
|
+
min: 256,
|
|
2304
|
+
max: 5242880,
|
|
2305
|
+
integer: true
|
|
2306
|
+
});
|
|
2307
|
+
const requestMethod = getRequestMethod(requestText);
|
|
2308
|
+
const authorization = parseNetworkAuthorization(args.authorization);
|
|
2309
|
+
const { target } = await resolveAuthorizedTransportTarget(`http://${formatHostForUrl(host)}:${String(port)}/`, authorization, "HTTP request");
|
|
2310
|
+
if (authorization) {
|
|
2311
|
+
const requestTarget = (requestText.split(/\r?\n/, 1)[0] ?? "").split(/\s+/)[1] ?? "";
|
|
2312
|
+
if (requestTarget.includes("://")) try {
|
|
2313
|
+
const targetHost = normalizeTargetHost(new URL(requestTarget).hostname);
|
|
2314
|
+
if (targetHost !== host && targetHost !== (target.resolvedAddress ?? "")) throw new Error(`HTTP request blocked: request-line target host "${targetHost}" does not match authorized host "${host}"`);
|
|
2315
|
+
} catch (e) {
|
|
2316
|
+
if (e instanceof Error && e.message.startsWith("HTTP request blocked:")) throw e;
|
|
2317
|
+
}
|
|
2318
|
+
const hostHeaderValue = requestText.match(/^Host:\s*(\S+)/im)?.[1];
|
|
2319
|
+
if (hostHeaderValue) {
|
|
2320
|
+
const declaredHost = normalizeTargetHost(hostHeaderValue.replace(/:\d+$/, ""));
|
|
2321
|
+
if (declaredHost !== host && declaredHost !== (target.resolvedAddress ?? "")) throw new Error(`HTTP request blocked: Host header "${hostHeaderValue}" does not match authorized host "${host}"`);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
const exchange = await exchangePlainHttp(target.resolvedAddress ?? target.hostname, port, Buffer.from(requestText, "utf8"), requestMethod, timeoutMs, maxResponseBytes);
|
|
2325
|
+
const analysis = analyzeHttpResponse(exchange.rawResponse, requestMethod);
|
|
2326
|
+
if (!analysis) throw new Error("Received data but could not parse complete HTTP response headers.");
|
|
2327
|
+
const bodyIsText = isLikelyTextHttpBody(analysis.rawHeaders.find((header) => header.name.toLowerCase() === "content-type")?.value ?? null, analysis.bodyBuffer);
|
|
2328
|
+
const complete = analysis.complete || analysis.bodyMode === "until-close" && exchange.endedBy === "socket-close";
|
|
2329
|
+
emitEvent(this.eventBus, "network:http_plain_request_completed", {
|
|
2330
|
+
host,
|
|
2331
|
+
port,
|
|
2332
|
+
statusCode: analysis.statusCode,
|
|
2333
|
+
byteLength: exchange.rawResponse.length,
|
|
2334
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2335
|
+
});
|
|
2336
|
+
return R.ok().merge({
|
|
2337
|
+
host,
|
|
2338
|
+
port,
|
|
2339
|
+
resolvedAddress: target.resolvedAddress ?? target.hostname,
|
|
2340
|
+
requestBytes: Buffer.byteLength(requestText, "utf8"),
|
|
2341
|
+
response: {
|
|
2342
|
+
statusLine: analysis.statusLine,
|
|
2343
|
+
httpVersion: analysis.httpVersion,
|
|
2344
|
+
statusCode: analysis.statusCode,
|
|
2345
|
+
statusText: analysis.statusText,
|
|
2346
|
+
headers: analysis.headers,
|
|
2347
|
+
rawHeaders: analysis.rawHeaders,
|
|
2348
|
+
headerBytes: analysis.headerBytes,
|
|
2349
|
+
bodyBytes: analysis.bodyBytes,
|
|
2350
|
+
bodyMode: analysis.bodyMode,
|
|
2351
|
+
chunkedDecoded: analysis.chunkedDecoded,
|
|
2352
|
+
complete,
|
|
2353
|
+
truncated: exchange.endedBy === "max-bytes",
|
|
2354
|
+
endedBy: exchange.endedBy,
|
|
2355
|
+
bodyText: bodyIsText ? analysis.bodyBuffer.toString("utf8") : void 0,
|
|
2356
|
+
bodyBase64: bodyIsText ? void 0 : analysis.bodyBuffer.toString("base64")
|
|
2357
|
+
}
|
|
2358
|
+
}).json();
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
2361
|
+
}
|
|
2433
2362
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
return {
|
|
2441
|
-
type: icmpType,
|
|
2442
|
-
code: icmpCode,
|
|
2443
|
-
fromIp
|
|
2444
|
-
};
|
|
2363
|
+
};
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region src/server/domains/network/handlers/raw-http2-handlers.ts
|
|
2366
|
+
var RawHttp2Handlers = class {
|
|
2367
|
+
constructor(eventBus) {
|
|
2368
|
+
this.eventBus = eventBus;
|
|
2445
2369
|
}
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
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) {
|
|
2370
|
+
async handleHttp2Probe(args) {
|
|
2371
|
+
const rawUrl = parseOptionalString(args.url, "url");
|
|
2372
|
+
let eventUrl = rawUrl ?? "";
|
|
2373
|
+
let eventStatusCode = null;
|
|
2374
|
+
let eventAlpnProtocol = null;
|
|
2375
|
+
let eventSuccess = false;
|
|
2734
2376
|
try {
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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
|
|
2377
|
+
if (!rawUrl) throw new Error("url is required");
|
|
2378
|
+
const method = (parseOptionalString(args.method, "method") ?? "GET").toUpperCase();
|
|
2379
|
+
if (!HTTP_TOKEN_RE.test(method)) throw new Error("method must be a valid HTTP token");
|
|
2380
|
+
const timeoutMs = parseNumberArg(args.timeoutMs, {
|
|
2381
|
+
defaultValue: 3e4,
|
|
2382
|
+
min: 1,
|
|
2383
|
+
max: 12e4,
|
|
2384
|
+
integer: true
|
|
2759
2385
|
});
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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
|
|
2386
|
+
const maxBodyBytes = parseNumberArg(args.maxBodyBytes, {
|
|
2387
|
+
defaultValue: 32768,
|
|
2388
|
+
min: 128,
|
|
2389
|
+
max: 1048576,
|
|
2390
|
+
integer: true
|
|
2776
2391
|
});
|
|
2777
|
-
|
|
2778
|
-
const
|
|
2779
|
-
|
|
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");
|
|
2392
|
+
const rawBody = args.body;
|
|
2393
|
+
const bodyIsObject = rawBody !== null && rawBody !== void 0 && typeof rawBody !== "string";
|
|
2394
|
+
const bodyString = typeof rawBody === "string" ? rawBody : bodyIsObject ? JSON.stringify(rawBody) : "";
|
|
2395
|
+
const bodyBuffer = Buffer.from(bodyString, "utf8");
|
|
2914
2396
|
const alpnProtocols = parseStringArray(args.alpnProtocols, "alpnProtocols");
|
|
2915
2397
|
const requestHeaders = toHttp2RequestHeaders(parseHeaderRecord(args.headers, "headers"));
|
|
2398
|
+
if (bodyIsObject && bodyBuffer.length > 0 && !("content-type" in requestHeaders)) requestHeaders["content-type"] = "application/json";
|
|
2916
2399
|
const { url, target } = await resolveAuthorizedTransportTarget(rawUrl, parseNetworkAuthorization(args.authorization), "HTTP/2 probe");
|
|
2917
2400
|
eventUrl = url.toString();
|
|
2918
2401
|
if (!("content-length" in requestHeaders) && bodyBuffer.length > 0) requestHeaders["content-length"] = String(bodyBuffer.length);
|
|
@@ -3024,32 +2507,594 @@ var RawHandlers = class {
|
|
|
3024
2507
|
};
|
|
3025
2508
|
});
|
|
3026
2509
|
}
|
|
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();
|
|
2510
|
+
const result = buildHttp2Frame({
|
|
2511
|
+
frameType,
|
|
2512
|
+
...streamId !== void 0 && { streamId },
|
|
2513
|
+
...flags !== void 0 && { flags },
|
|
2514
|
+
...frameTypeCode !== void 0 && { frameTypeCode },
|
|
2515
|
+
...payloadHex !== void 0 && { payloadHex },
|
|
2516
|
+
...payloadText !== void 0 && { payloadText },
|
|
2517
|
+
...payloadEncoding !== void 0 && { payloadEncoding },
|
|
2518
|
+
...settings !== void 0 && { settings },
|
|
2519
|
+
...ack !== void 0 && { ack },
|
|
2520
|
+
...pingOpaqueDataHex !== void 0 && { pingOpaqueDataHex },
|
|
2521
|
+
...windowSizeIncrement !== void 0 && { windowSizeIncrement },
|
|
2522
|
+
...errorCode !== void 0 && { errorCode },
|
|
2523
|
+
...lastStreamId !== void 0 && { lastStreamId },
|
|
2524
|
+
...debugDataText !== void 0 && { debugDataText },
|
|
2525
|
+
...debugDataEncoding !== void 0 && { debugDataEncoding }
|
|
2526
|
+
});
|
|
2527
|
+
emitEvent(this.eventBus, "network:http2_frame_build_completed", {
|
|
2528
|
+
frameType: result.frameType,
|
|
2529
|
+
typeCode: result.typeCode,
|
|
2530
|
+
streamId: result.streamId,
|
|
2531
|
+
flags: result.flags,
|
|
2532
|
+
payloadBytes: result.payloadBytes,
|
|
2533
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2534
|
+
});
|
|
2535
|
+
return R.ok().merge(result).json();
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
//#endregion
|
|
2539
|
+
//#region src/native/IcmpProbe.ts
|
|
2540
|
+
/**
|
|
2541
|
+
* Cross-platform ICMP probe and traceroute via koffi FFI.
|
|
2542
|
+
*
|
|
2543
|
+
* Windows: IcmpSendEcho from iphlpapi.dll (no admin required).
|
|
2544
|
+
* Linux/macOS: Raw ICMP sockets via libc (requires root/CAP_NET_RAW).
|
|
2545
|
+
*
|
|
2546
|
+
* Uses Buffer-based struct parsing (same pattern as Win32API.ts)
|
|
2547
|
+
* to avoid koffi struct registration issues in test environments.
|
|
2548
|
+
*/
|
|
2549
|
+
function ipToString(addr) {
|
|
2550
|
+
return `${addr & 255}.${addr >>> 8 & 255}.${addr >>> 16 & 255}.${addr >>> 24 & 255}`;
|
|
2551
|
+
}
|
|
2552
|
+
function isValidIpv4(ip) {
|
|
2553
|
+
const parts = ip.split(".");
|
|
2554
|
+
if (parts.length !== 4) return false;
|
|
2555
|
+
return parts.every((p) => {
|
|
2556
|
+
const n = parseInt(p, 10);
|
|
2557
|
+
return !isNaN(n) && n >= 0 && n <= 255 && p === String(n);
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
let available = null;
|
|
2561
|
+
const IP_STATUS = {
|
|
2562
|
+
0: "SUCCESS",
|
|
2563
|
+
11001: "BUF_TOO_SMALL",
|
|
2564
|
+
11002: "DEST_NET_UNREACHABLE",
|
|
2565
|
+
11003: "DEST_HOST_UNREACHABLE",
|
|
2566
|
+
11004: "DEST_PROT_UNREACHABLE",
|
|
2567
|
+
11005: "DEST_PORT_UNREACHABLE",
|
|
2568
|
+
11009: "PACKET_TOO_BIG",
|
|
2569
|
+
11010: "REQ_TIMED_OUT",
|
|
2570
|
+
11013: "TTL_EXPIRED_TRANSIT",
|
|
2571
|
+
11014: "TTL_EXPIRED_REASSEM",
|
|
2572
|
+
11015: "PARAM_PROBLEM",
|
|
2573
|
+
11016: "SOURCE_QUENCH",
|
|
2574
|
+
11050: "GENERAL_FAILURE"
|
|
2575
|
+
};
|
|
2576
|
+
function winStatusLabel(s) {
|
|
2577
|
+
return IP_STATUS[s] ?? `UNKNOWN_${s}`;
|
|
2578
|
+
}
|
|
2579
|
+
function winStatusClass(s) {
|
|
2580
|
+
if (s === 0) return "success";
|
|
2581
|
+
if (s === 11010) return "timeout";
|
|
2582
|
+
if (s === 11013 || s === 11014) return "time_exceeded";
|
|
2583
|
+
if (s >= 11002 && s <= 11005) return "destination_unreachable";
|
|
2584
|
+
if (s === 11016) return "source_quench";
|
|
2585
|
+
if (s === 11009) return "packet_too_big";
|
|
2586
|
+
if (s === 11015) return "parameter_problem";
|
|
2587
|
+
return "error";
|
|
2588
|
+
}
|
|
2589
|
+
let iphlpapi = null;
|
|
2590
|
+
let ws2_32 = null;
|
|
2591
|
+
function getIphlpapi() {
|
|
2592
|
+
if (!iphlpapi) {
|
|
2593
|
+
iphlpapi = koffi.load("iphlpapi.dll");
|
|
2594
|
+
logger.debug("Loaded iphlpapi.dll via koffi");
|
|
2595
|
+
}
|
|
2596
|
+
return iphlpapi;
|
|
2597
|
+
}
|
|
2598
|
+
function getWs2_32() {
|
|
2599
|
+
if (!ws2_32) {
|
|
2600
|
+
ws2_32 = koffi.load("ws2_32.dll");
|
|
2601
|
+
logger.debug("Loaded ws2_32.dll via koffi");
|
|
2602
|
+
}
|
|
2603
|
+
return ws2_32;
|
|
2604
|
+
}
|
|
2605
|
+
const IP_OPT_SIZE = 16;
|
|
2606
|
+
const MIN_REPLY_BUF_SIZE = 256;
|
|
2607
|
+
const ICMP_REPLY_OVERHEAD = 64;
|
|
2608
|
+
function getReplyBufferSize(packetSize) {
|
|
2609
|
+
return Math.max(MIN_REPLY_BUF_SIZE, packetSize + ICMP_REPLY_OVERHEAD);
|
|
2610
|
+
}
|
|
2611
|
+
function buildOptionBuf(ttl) {
|
|
2612
|
+
const buf = Buffer.alloc(IP_OPT_SIZE, 0);
|
|
2613
|
+
buf.writeUInt8(ttl, 0);
|
|
2614
|
+
return buf;
|
|
2615
|
+
}
|
|
2616
|
+
function parseReply(buf) {
|
|
2617
|
+
return {
|
|
2618
|
+
address: buf.readUInt32LE(0),
|
|
2619
|
+
status: buf.readUInt32LE(4),
|
|
2620
|
+
rtt: buf.readUInt32LE(8)
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
function win_inet_addr(ip) {
|
|
2624
|
+
return getWs2_32().func("uint32 inet_addr(char *)")(ip);
|
|
2625
|
+
}
|
|
2626
|
+
function win_IcmpCreateFile() {
|
|
2627
|
+
return getIphlpapi().func("void * IcmpCreateFile()")();
|
|
2628
|
+
}
|
|
2629
|
+
function win_IcmpCloseHandle(h) {
|
|
2630
|
+
return getIphlpapi().func("int IcmpCloseHandle(void *)")(h) !== 0;
|
|
2631
|
+
}
|
|
2632
|
+
function win_IcmpSendEcho(handle, destAddr, sendData, optionBuf, timeoutMs) {
|
|
2633
|
+
const fn = getIphlpapi().func("uint32 IcmpSendEcho(void *, uint32, void *, uint16, void *, void *, uint32, uint32)");
|
|
2634
|
+
const replyBuf = Buffer.alloc(getReplyBufferSize(sendData.length));
|
|
2635
|
+
const n = fn(handle, destAddr, sendData, sendData.length, optionBuf, replyBuf, replyBuf.length, timeoutMs);
|
|
2636
|
+
return {
|
|
2637
|
+
numReplies: Number(n),
|
|
2638
|
+
replyBuf
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
function winIcmpProbe(params) {
|
|
2642
|
+
const { target, ttl = 128, packetSize = ICMP_DEFAULT_PACKET_SIZE, timeout = ICMP_PROBE_TIMEOUT_MS } = params;
|
|
2643
|
+
const destAddr = win_inet_addr(target);
|
|
2644
|
+
if (destAddr === 4294967295) return {
|
|
2645
|
+
target,
|
|
2646
|
+
ip: "",
|
|
2647
|
+
alive: false,
|
|
2648
|
+
rtt: null,
|
|
2649
|
+
ttl,
|
|
2650
|
+
icmpStatus: "INVALID_ADDRESS",
|
|
2651
|
+
errorClass: "error",
|
|
2652
|
+
packetSize
|
|
2653
|
+
};
|
|
2654
|
+
const handle = win_IcmpCreateFile();
|
|
2655
|
+
try {
|
|
2656
|
+
const { numReplies, replyBuf } = win_IcmpSendEcho(handle, destAddr, Buffer.alloc(packetSize, 170), buildOptionBuf(ttl), timeout);
|
|
2657
|
+
if (numReplies === 0) return {
|
|
2658
|
+
target,
|
|
2659
|
+
ip: ipToString(destAddr),
|
|
2660
|
+
alive: false,
|
|
2661
|
+
rtt: null,
|
|
2662
|
+
ttl,
|
|
2663
|
+
icmpStatus: "REQ_TIMED_OUT",
|
|
2664
|
+
errorClass: "timeout",
|
|
2665
|
+
packetSize
|
|
2666
|
+
};
|
|
2667
|
+
const reply = parseReply(replyBuf);
|
|
2668
|
+
return {
|
|
2669
|
+
target,
|
|
2670
|
+
ip: ipToString(reply.address),
|
|
2671
|
+
alive: reply.status === 0,
|
|
2672
|
+
rtt: reply.status === 0 ? reply.rtt : null,
|
|
2673
|
+
ttl,
|
|
2674
|
+
icmpStatus: winStatusLabel(reply.status),
|
|
2675
|
+
errorClass: winStatusClass(reply.status),
|
|
2676
|
+
packetSize
|
|
2677
|
+
};
|
|
2678
|
+
} finally {
|
|
2679
|
+
win_IcmpCloseHandle(handle);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
function winTraceroute(params) {
|
|
2683
|
+
const { target, maxHops = ICMP_TRACEROUTE_MAX_HOPS, timeout = ICMP_PROBE_TIMEOUT_MS, packetSize = ICMP_DEFAULT_PACKET_SIZE } = params;
|
|
2684
|
+
const destAddr = win_inet_addr(target);
|
|
2685
|
+
if (destAddr === 4294967295) return {
|
|
2686
|
+
target,
|
|
2687
|
+
ip: "",
|
|
2688
|
+
hops: [],
|
|
2689
|
+
reached: false,
|
|
2690
|
+
totalHops: 0,
|
|
2691
|
+
totalTime: 0
|
|
2692
|
+
};
|
|
2693
|
+
const handle = win_IcmpCreateFile();
|
|
2694
|
+
const hops = [];
|
|
2695
|
+
const t0 = performance.now();
|
|
2696
|
+
try {
|
|
2697
|
+
for (let ttl = 1; ttl <= maxHops; ttl++) {
|
|
2698
|
+
const { numReplies, replyBuf } = win_IcmpSendEcho(handle, destAddr, Buffer.alloc(packetSize, 170), buildOptionBuf(ttl), timeout);
|
|
2699
|
+
if (numReplies === 0) {
|
|
2700
|
+
hops.push({
|
|
2701
|
+
hop: ttl,
|
|
2702
|
+
ip: null,
|
|
2703
|
+
rtt: null,
|
|
2704
|
+
status: "REQ_TIMED_OUT",
|
|
2705
|
+
errorClass: "timeout"
|
|
2706
|
+
});
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
const reply = parseReply(replyBuf);
|
|
2710
|
+
const hopIp = ipToString(reply.address);
|
|
2711
|
+
hops.push({
|
|
2712
|
+
hop: ttl,
|
|
2713
|
+
ip: hopIp,
|
|
2714
|
+
rtt: reply.rtt,
|
|
2715
|
+
status: winStatusLabel(reply.status),
|
|
2716
|
+
errorClass: winStatusClass(reply.status)
|
|
2717
|
+
});
|
|
2718
|
+
if (reply.status === 0) break;
|
|
2719
|
+
}
|
|
2720
|
+
} finally {
|
|
2721
|
+
win_IcmpCloseHandle(handle);
|
|
2722
|
+
}
|
|
2723
|
+
const last = hops[hops.length - 1];
|
|
2724
|
+
return {
|
|
2725
|
+
target,
|
|
2726
|
+
ip: ipToString(destAddr),
|
|
2727
|
+
hops,
|
|
2728
|
+
reached: last?.status === "SUCCESS",
|
|
2729
|
+
totalHops: hops.length,
|
|
2730
|
+
totalTime: Math.round((performance.now() - t0) * 100) / 100
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
const AF_INET = 2;
|
|
2734
|
+
const SOCK_RAW = 3;
|
|
2735
|
+
const IPPROTO_ICMP = 1;
|
|
2736
|
+
const IPPROTO_IP = 0;
|
|
2737
|
+
const IP_TTL = 2;
|
|
2738
|
+
const SOL_SOCKET = 1;
|
|
2739
|
+
const SO_RCVTIMEO = process.platform === "darwin" ? 4102 : 20;
|
|
2740
|
+
const POSIX_LIB = process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6";
|
|
2741
|
+
let posixLib = null;
|
|
2742
|
+
function getPosixLib() {
|
|
2743
|
+
if (!posixLib) {
|
|
2744
|
+
posixLib = koffi.load(POSIX_LIB);
|
|
2745
|
+
logger.debug(`Loaded ${POSIX_LIB} via koffi for ICMP`);
|
|
2746
|
+
}
|
|
2747
|
+
return posixLib;
|
|
2748
|
+
}
|
|
2749
|
+
function posixSocket(domain, type, protocol) {
|
|
2750
|
+
return getPosixLib().func("int socket(int, int, int)")(domain, type, protocol);
|
|
2751
|
+
}
|
|
2752
|
+
function posixSetsockopt(fd, level, optname, optval, optlen) {
|
|
2753
|
+
return getPosixLib().func("int setsockopt(int, int, int, void *, int)")(fd, level, optname, optval, optlen);
|
|
2754
|
+
}
|
|
2755
|
+
function posixSendto(fd, buf, addr) {
|
|
2756
|
+
return getPosixLib().func("int sendto(int, void *, int, int, void *, int)")(fd, buf, buf.length, 0, addr, 16);
|
|
2757
|
+
}
|
|
2758
|
+
function posixRecv(fd, buf) {
|
|
2759
|
+
return getPosixLib().func("int recv(int, void *, int, int)")(fd, buf, buf.length, 0);
|
|
2760
|
+
}
|
|
2761
|
+
function posixClose(fd) {
|
|
2762
|
+
return getPosixLib().func("int close(int)")(fd);
|
|
2763
|
+
}
|
|
2764
|
+
function computeChecksum(buf) {
|
|
2765
|
+
let sum = 0;
|
|
2766
|
+
for (let i = 0; i < buf.length - 1; i += 2) sum += buf.readUInt16BE(i);
|
|
2767
|
+
if (buf.length & 1) sum += (buf[buf.length - 1] ?? 0) << 8;
|
|
2768
|
+
while (sum > 65535) sum = (sum & 65535) + (sum >>> 16);
|
|
2769
|
+
return ~sum & 65535;
|
|
2770
|
+
}
|
|
2771
|
+
function buildIcmpEcho(id, seq, payloadSize) {
|
|
2772
|
+
const buf = Buffer.alloc(8 + payloadSize);
|
|
2773
|
+
buf[0] = 8;
|
|
2774
|
+
buf[1] = 0;
|
|
2775
|
+
buf.writeUInt16BE(id & 65535, 4);
|
|
2776
|
+
buf.writeUInt16BE(seq & 65535, 6);
|
|
2777
|
+
for (let i = 8; i < buf.length; i++) buf[i] = 170;
|
|
2778
|
+
buf.writeUInt16BE(computeChecksum(buf), 2);
|
|
2779
|
+
return buf;
|
|
2780
|
+
}
|
|
2781
|
+
function buildSockaddrIn(ip) {
|
|
2782
|
+
const buf = Buffer.alloc(16, 0);
|
|
2783
|
+
buf.writeUInt16LE(AF_INET, 0);
|
|
2784
|
+
const parts = ip.split(".").map(Number);
|
|
2785
|
+
buf[4] = parts[0] ?? 0;
|
|
2786
|
+
buf[5] = parts[1] ?? 0;
|
|
2787
|
+
buf[6] = parts[2] ?? 0;
|
|
2788
|
+
buf[7] = parts[3] ?? 0;
|
|
2789
|
+
return buf;
|
|
2790
|
+
}
|
|
2791
|
+
function parseIcmpPacket(buf, n, expectedId) {
|
|
2792
|
+
if (n < 20) return null;
|
|
2793
|
+
const ihl = ((buf[0] ?? 0) & 15) * 4;
|
|
2794
|
+
if (n < ihl + 8) return null;
|
|
2795
|
+
const icmpType = buf[ihl] ?? 0;
|
|
2796
|
+
const icmpCode = buf[ihl + 1] ?? 0;
|
|
2797
|
+
const fromIp = buf.readUInt32LE(12);
|
|
2798
|
+
if (icmpType === 0) {
|
|
2799
|
+
if (buf.readUInt16BE(ihl + 4) !== expectedId) return null;
|
|
2800
|
+
return {
|
|
2801
|
+
type: icmpType,
|
|
2802
|
+
code: icmpCode,
|
|
2803
|
+
fromIp
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
if (icmpType === 11 || icmpType === 3) {
|
|
2807
|
+
const origStart = ihl + 8;
|
|
2808
|
+
if (n < origStart + 28) return null;
|
|
2809
|
+
const origIhl = ((buf[origStart] ?? 0) & 15) * 4;
|
|
2810
|
+
if (n < origStart + origIhl + 8) return null;
|
|
2811
|
+
if (buf.readUInt16BE(origStart + origIhl + 4) !== expectedId) return null;
|
|
2812
|
+
return {
|
|
2813
|
+
type: icmpType,
|
|
2814
|
+
code: icmpCode,
|
|
2815
|
+
fromIp
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
function posixStatusLabel(type, code, timedOut) {
|
|
2821
|
+
if (type === 0) return "SUCCESS";
|
|
2822
|
+
if (timedOut) return "REQ_TIMED_OUT";
|
|
2823
|
+
if (type === 11 && code === 0) return "TTL_EXPIRED_TRANSIT";
|
|
2824
|
+
if (type === 11 && code === 1) return "TTL_EXPIRED_REASSEM";
|
|
2825
|
+
if (type === 3 && code === 0) return "DEST_NET_UNREACHABLE";
|
|
2826
|
+
if (type === 3 && code === 1) return "DEST_HOST_UNREACHABLE";
|
|
2827
|
+
if (type === 3 && code === 2) return "DEST_PROT_UNREACHABLE";
|
|
2828
|
+
if (type === 3 && code === 3) return "DEST_PORT_UNREACHABLE";
|
|
2829
|
+
return `UNKNOWN_${type}_${code}`;
|
|
2830
|
+
}
|
|
2831
|
+
function posixErrorClass(type, _exitCodeValue, timedOut) {
|
|
2832
|
+
if (type === 0) return "success";
|
|
2833
|
+
if (timedOut) return "timeout";
|
|
2834
|
+
if (type === 11) return "time_exceeded";
|
|
2835
|
+
if (type === 3) return "destination_unreachable";
|
|
2836
|
+
return "error";
|
|
2837
|
+
}
|
|
2838
|
+
function posixSetTtl(fd, ttl) {
|
|
2839
|
+
const buf = Buffer.alloc(4);
|
|
2840
|
+
buf.writeInt32LE(ttl);
|
|
2841
|
+
posixSetsockopt(fd, IPPROTO_IP, IP_TTL, buf, 4);
|
|
2842
|
+
}
|
|
2843
|
+
function posixSetRecvTimeout(fd, timeoutMs) {
|
|
2844
|
+
const tv = Buffer.alloc(16, 0);
|
|
2845
|
+
tv.writeInt32LE(Math.floor(timeoutMs / 1e3), 0);
|
|
2846
|
+
tv.writeInt32LE(timeoutMs % 1e3 * 1e3, 8);
|
|
2847
|
+
posixSetsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv, 16);
|
|
2848
|
+
}
|
|
2849
|
+
function posixIcmpProbe(params) {
|
|
2850
|
+
const { target, ttl = 128, packetSize = ICMP_DEFAULT_PACKET_SIZE, timeout = ICMP_PROBE_TIMEOUT_MS } = params;
|
|
2851
|
+
if (!isValidIpv4(target)) return {
|
|
2852
|
+
target,
|
|
2853
|
+
ip: "",
|
|
2854
|
+
alive: false,
|
|
2855
|
+
rtt: null,
|
|
2856
|
+
ttl,
|
|
2857
|
+
icmpStatus: "INVALID_ADDRESS",
|
|
2858
|
+
errorClass: "error",
|
|
2859
|
+
packetSize
|
|
2860
|
+
};
|
|
2861
|
+
const fd = posixSocket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
2862
|
+
if (fd < 0) return {
|
|
2863
|
+
target,
|
|
2864
|
+
ip: "",
|
|
2865
|
+
alive: false,
|
|
2866
|
+
rtt: null,
|
|
2867
|
+
ttl,
|
|
2868
|
+
icmpStatus: "SOCKET_ERROR",
|
|
2869
|
+
errorClass: "error",
|
|
2870
|
+
packetSize
|
|
2871
|
+
};
|
|
2872
|
+
try {
|
|
2873
|
+
posixSetTtl(fd, ttl);
|
|
2874
|
+
posixSetRecvTimeout(fd, timeout);
|
|
2875
|
+
const id = process.pid & 65535;
|
|
2876
|
+
const packet = buildIcmpEcho(id, 1, packetSize);
|
|
2877
|
+
const destAddr = buildSockaddrIn(target);
|
|
2878
|
+
const t0 = performance.now();
|
|
2879
|
+
if (posixSendto(fd, packet, destAddr) < 0) return {
|
|
2880
|
+
target,
|
|
2881
|
+
ip: target,
|
|
2882
|
+
alive: false,
|
|
2883
|
+
rtt: null,
|
|
2884
|
+
ttl,
|
|
2885
|
+
icmpStatus: "SEND_ERROR",
|
|
2886
|
+
errorClass: "error",
|
|
2887
|
+
packetSize
|
|
2888
|
+
};
|
|
2889
|
+
const recvBuf = Buffer.alloc(512);
|
|
2890
|
+
const n = posixRecv(fd, recvBuf);
|
|
2891
|
+
const rtt = Math.round(performance.now() - t0);
|
|
2892
|
+
if (n <= 0) return {
|
|
2893
|
+
target,
|
|
2894
|
+
ip: target,
|
|
2895
|
+
alive: false,
|
|
2896
|
+
rtt: null,
|
|
2897
|
+
ttl,
|
|
2898
|
+
icmpStatus: "REQ_TIMED_OUT",
|
|
2899
|
+
errorClass: "timeout",
|
|
2900
|
+
packetSize
|
|
2901
|
+
};
|
|
2902
|
+
const reply = parseIcmpPacket(recvBuf, n, id);
|
|
2903
|
+
if (!reply) return {
|
|
2904
|
+
target,
|
|
2905
|
+
ip: target,
|
|
2906
|
+
alive: false,
|
|
2907
|
+
rtt: null,
|
|
2908
|
+
ttl,
|
|
2909
|
+
icmpStatus: "UNEXPECTED_REPLY",
|
|
2910
|
+
errorClass: "error",
|
|
2911
|
+
packetSize
|
|
2912
|
+
};
|
|
2913
|
+
const alive = reply.type === 0;
|
|
2914
|
+
return {
|
|
2915
|
+
target,
|
|
2916
|
+
ip: ipToString(reply.fromIp),
|
|
2917
|
+
alive,
|
|
2918
|
+
rtt: alive ? rtt : null,
|
|
2919
|
+
ttl,
|
|
2920
|
+
icmpStatus: posixStatusLabel(reply.type, reply.code, false),
|
|
2921
|
+
errorClass: posixErrorClass(reply.type, reply.code, false),
|
|
2922
|
+
packetSize
|
|
2923
|
+
};
|
|
2924
|
+
} finally {
|
|
2925
|
+
posixClose(fd);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
function posixTraceroute(params) {
|
|
2929
|
+
const { target, maxHops = ICMP_TRACEROUTE_MAX_HOPS, timeout = ICMP_PROBE_TIMEOUT_MS, packetSize = ICMP_DEFAULT_PACKET_SIZE } = params;
|
|
2930
|
+
if (!isValidIpv4(target)) return {
|
|
2931
|
+
target,
|
|
2932
|
+
ip: "",
|
|
2933
|
+
hops: [],
|
|
2934
|
+
reached: false,
|
|
2935
|
+
totalHops: 0,
|
|
2936
|
+
totalTime: 0
|
|
2937
|
+
};
|
|
2938
|
+
const fd = posixSocket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
2939
|
+
if (fd < 0) return {
|
|
2940
|
+
target,
|
|
2941
|
+
ip: "",
|
|
2942
|
+
hops: [],
|
|
2943
|
+
reached: false,
|
|
2944
|
+
totalHops: 0,
|
|
2945
|
+
totalTime: 0
|
|
2946
|
+
};
|
|
2947
|
+
const hops = [];
|
|
2948
|
+
const id = process.pid & 65535;
|
|
2949
|
+
const destAddr = buildSockaddrIn(target);
|
|
2950
|
+
const t0 = performance.now();
|
|
2951
|
+
const MAX_CONSECUTIVE_SEND_ERRORS = 5;
|
|
2952
|
+
let consecutiveSendErrors = 0;
|
|
2953
|
+
try {
|
|
2954
|
+
posixSetRecvTimeout(fd, timeout);
|
|
2955
|
+
for (let ttl = 1; ttl <= maxHops; ttl++) {
|
|
2956
|
+
posixSetTtl(fd, ttl);
|
|
2957
|
+
const packet = buildIcmpEcho(id, ttl, packetSize);
|
|
2958
|
+
const sendT0 = performance.now();
|
|
2959
|
+
if (posixSendto(fd, packet, destAddr) < 0) {
|
|
2960
|
+
consecutiveSendErrors++;
|
|
2961
|
+
hops.push({
|
|
2962
|
+
hop: ttl,
|
|
2963
|
+
ip: null,
|
|
2964
|
+
rtt: null,
|
|
2965
|
+
status: "SEND_ERROR",
|
|
2966
|
+
errorClass: "error"
|
|
2967
|
+
});
|
|
2968
|
+
if (consecutiveSendErrors >= MAX_CONSECUTIVE_SEND_ERRORS) break;
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
consecutiveSendErrors = 0;
|
|
2972
|
+
const recvBuf = Buffer.alloc(512);
|
|
2973
|
+
const n = posixRecv(fd, recvBuf);
|
|
2974
|
+
const rtt = Math.round(performance.now() - sendT0);
|
|
2975
|
+
if (n <= 0) {
|
|
2976
|
+
hops.push({
|
|
2977
|
+
hop: ttl,
|
|
2978
|
+
ip: null,
|
|
2979
|
+
rtt: null,
|
|
2980
|
+
status: "REQ_TIMED_OUT",
|
|
2981
|
+
errorClass: "timeout"
|
|
2982
|
+
});
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
const reply = parseIcmpPacket(recvBuf, n, id);
|
|
2986
|
+
if (!reply) {
|
|
2987
|
+
hops.push({
|
|
2988
|
+
hop: ttl,
|
|
2989
|
+
ip: null,
|
|
2990
|
+
rtt: null,
|
|
2991
|
+
status: "UNEXPECTED_REPLY",
|
|
2992
|
+
errorClass: "error"
|
|
2993
|
+
});
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
const status = posixStatusLabel(reply.type, reply.code, false);
|
|
2997
|
+
const errorCls = posixErrorClass(reply.type, reply.code, false);
|
|
2998
|
+
hops.push({
|
|
2999
|
+
hop: ttl,
|
|
3000
|
+
ip: ipToString(reply.fromIp),
|
|
3001
|
+
rtt,
|
|
3002
|
+
status,
|
|
3003
|
+
errorClass: errorCls
|
|
3004
|
+
});
|
|
3005
|
+
if (reply.type === 0) break;
|
|
3006
|
+
}
|
|
3007
|
+
} finally {
|
|
3008
|
+
posixClose(fd);
|
|
3009
|
+
}
|
|
3010
|
+
return {
|
|
3011
|
+
target,
|
|
3012
|
+
ip: target,
|
|
3013
|
+
hops,
|
|
3014
|
+
reached: hops[hops.length - 1]?.status === "SUCCESS",
|
|
3015
|
+
totalHops: hops.length,
|
|
3016
|
+
totalTime: Math.round((performance.now() - t0) * 100) / 100
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
const isPosix = process.platform === "linux" || process.platform === "darwin";
|
|
3020
|
+
function isIcmpAvailable() {
|
|
3021
|
+
if (available !== null) return available;
|
|
3022
|
+
if (process.platform === "win32") try {
|
|
3023
|
+
koffi.load("iphlpapi.dll").unload();
|
|
3024
|
+
available = true;
|
|
3025
|
+
return true;
|
|
3026
|
+
} catch {
|
|
3027
|
+
available = false;
|
|
3028
|
+
return false;
|
|
3029
|
+
}
|
|
3030
|
+
if (isPosix) {
|
|
3031
|
+
try {
|
|
3032
|
+
const fd = posixSocket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
3033
|
+
if (fd >= 0) {
|
|
3034
|
+
posixClose(fd);
|
|
3035
|
+
available = true;
|
|
3036
|
+
} else available = false;
|
|
3037
|
+
} catch {
|
|
3038
|
+
available = false;
|
|
3039
|
+
}
|
|
3040
|
+
return available;
|
|
3041
|
+
}
|
|
3042
|
+
available = false;
|
|
3043
|
+
return false;
|
|
3044
|
+
}
|
|
3045
|
+
function icmpProbe(params) {
|
|
3046
|
+
const { target, ttl = 128, packetSize = ICMP_DEFAULT_PACKET_SIZE, timeout = ICMP_PROBE_TIMEOUT_MS } = params;
|
|
3047
|
+
if (!isIcmpAvailable()) return {
|
|
3048
|
+
target,
|
|
3049
|
+
ip: "",
|
|
3050
|
+
alive: false,
|
|
3051
|
+
rtt: null,
|
|
3052
|
+
ttl,
|
|
3053
|
+
icmpStatus: "PLATFORM_NOT_SUPPORTED",
|
|
3054
|
+
errorClass: "error",
|
|
3055
|
+
packetSize
|
|
3056
|
+
};
|
|
3057
|
+
if (process.platform === "win32") return winIcmpProbe({
|
|
3058
|
+
target,
|
|
3059
|
+
ttl,
|
|
3060
|
+
packetSize,
|
|
3061
|
+
timeout
|
|
3062
|
+
});
|
|
3063
|
+
return posixIcmpProbe({
|
|
3064
|
+
target,
|
|
3065
|
+
ttl,
|
|
3066
|
+
packetSize,
|
|
3067
|
+
timeout
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
function traceroute(params) {
|
|
3071
|
+
const { target, maxHops = ICMP_TRACEROUTE_MAX_HOPS, timeout = ICMP_PROBE_TIMEOUT_MS, packetSize = ICMP_DEFAULT_PACKET_SIZE } = params;
|
|
3072
|
+
if (!isIcmpAvailable()) return {
|
|
3073
|
+
target,
|
|
3074
|
+
ip: "",
|
|
3075
|
+
hops: [],
|
|
3076
|
+
reached: false,
|
|
3077
|
+
totalHops: 0,
|
|
3078
|
+
totalTime: 0
|
|
3079
|
+
};
|
|
3080
|
+
if (process.platform === "win32") return winTraceroute({
|
|
3081
|
+
target,
|
|
3082
|
+
maxHops,
|
|
3083
|
+
timeout,
|
|
3084
|
+
packetSize
|
|
3085
|
+
});
|
|
3086
|
+
return posixTraceroute({
|
|
3087
|
+
target,
|
|
3088
|
+
maxHops,
|
|
3089
|
+
timeout,
|
|
3090
|
+
packetSize
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
//#endregion
|
|
3094
|
+
//#region src/server/domains/network/handlers/raw-latency-handlers.ts
|
|
3095
|
+
var RawLatencyHandlers = class {
|
|
3096
|
+
constructor(eventBus) {
|
|
3097
|
+
this.eventBus = eventBus;
|
|
3053
3098
|
}
|
|
3054
3099
|
async handleNetworkRttMeasure(args) {
|
|
3055
3100
|
const urlRaw = parseOptionalString(args.url, "url");
|
|
@@ -3077,7 +3122,7 @@ var RawHandlers = class {
|
|
|
3077
3122
|
const useHttps = url.protocol === "https:";
|
|
3078
3123
|
const samples = [];
|
|
3079
3124
|
const errors = [];
|
|
3080
|
-
for (let
|
|
3125
|
+
for (let index = 0; index < iterations; index += 1) try {
|
|
3081
3126
|
const rtt = await this.measureSingleRtt(hostname, resolvedIp, port, probeType, timeoutMs, useHttps);
|
|
3082
3127
|
samples.push(rtt);
|
|
3083
3128
|
} catch (err) {
|
|
@@ -3106,6 +3151,46 @@ var RawHandlers = class {
|
|
|
3106
3151
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3107
3152
|
}).json();
|
|
3108
3153
|
}
|
|
3154
|
+
async handleNetworkTraceroute(args) {
|
|
3155
|
+
try {
|
|
3156
|
+
if (!isIcmpAvailable()) return R.text("ICMP traceroute not available on this platform (Windows: native API, Linux/macOS: requires root/CAP_NET_RAW)", true);
|
|
3157
|
+
const target = parseOptionalString(args.target, "target");
|
|
3158
|
+
if (!target) return R.text("target is required", true);
|
|
3159
|
+
const resolvedTarget = await resolveHostname(target);
|
|
3160
|
+
if (!resolvedTarget) return R.fail(`Could not resolve hostname: ${target}`).json();
|
|
3161
|
+
const result = traceroute({
|
|
3162
|
+
target: resolvedTarget,
|
|
3163
|
+
maxHops: clamp(args.maxHops !== void 0 ? Number(args.maxHops) : 30, 1, 64),
|
|
3164
|
+
timeout: clamp(args.timeout !== void 0 ? Number(args.timeout) : 5e3, 100, 3e4),
|
|
3165
|
+
packetSize: clamp(args.packetSize !== void 0 ? Number(args.packetSize) : 32, 8, 65500)
|
|
3166
|
+
});
|
|
3167
|
+
return R.ok().merge({ resolvedFrom: target }).json(result);
|
|
3168
|
+
} catch (err) {
|
|
3169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3170
|
+
return R.fail(`Traceroute failed: ${message}`).json();
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
async handleNetworkIcmpProbe(args) {
|
|
3174
|
+
try {
|
|
3175
|
+
if (!isIcmpAvailable()) return R.text("ICMP probe not available on this platform (Windows: native API, Linux/macOS: requires root/CAP_NET_RAW)", true);
|
|
3176
|
+
const target = parseOptionalString(args.target, "target");
|
|
3177
|
+
if (!target) return R.text("target is required", true);
|
|
3178
|
+
const resolvedTarget = await resolveHostname(target);
|
|
3179
|
+
if (!resolvedTarget) return R.fail(`Could not resolve hostname: ${target}`).json();
|
|
3180
|
+
const ttl = clamp(args.ttl !== void 0 ? Number(args.ttl) : 128, 1, 255);
|
|
3181
|
+
const timeout = clamp(args.timeout !== void 0 ? Number(args.timeout) : 5e3, 100, 3e4);
|
|
3182
|
+
const result = icmpProbe({
|
|
3183
|
+
target: resolvedTarget,
|
|
3184
|
+
ttl,
|
|
3185
|
+
packetSize: clamp(args.packetSize !== void 0 ? Number(args.packetSize) : 32, 8, 65500),
|
|
3186
|
+
timeout
|
|
3187
|
+
});
|
|
3188
|
+
return R.ok().merge({ resolvedFrom: target }).json(result);
|
|
3189
|
+
} catch (err) {
|
|
3190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3191
|
+
return R.fail(`ICMP probe failed: ${message}`).json();
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3109
3194
|
measureSingleRtt(hostname, address, port, probeType, timeoutMs, useHttps) {
|
|
3110
3195
|
switch (probeType) {
|
|
3111
3196
|
case "tcp": return this.probeTcp(address, port, timeoutMs);
|
|
@@ -3198,42 +3283,458 @@ var RawHandlers = class {
|
|
|
3198
3283
|
request.end();
|
|
3199
3284
|
});
|
|
3200
3285
|
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3286
|
+
};
|
|
3287
|
+
async function resolveHostname(target) {
|
|
3288
|
+
if (net.isIPv4(target)) return target;
|
|
3289
|
+
try {
|
|
3290
|
+
return (await dns.resolve(target, "A"))[0] ?? null;
|
|
3291
|
+
} catch {
|
|
3292
|
+
return null;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
//#endregion
|
|
3296
|
+
//#region src/server/domains/network/handlers/raw-handlers.ts
|
|
3297
|
+
var RawHandlers = class extends RawLatencyHandlers {
|
|
3298
|
+
dnsHttp;
|
|
3299
|
+
http2;
|
|
3300
|
+
constructor(eventBus) {
|
|
3301
|
+
super(eventBus);
|
|
3302
|
+
this.dnsHttp = new RawDnsHttpHandlers(eventBus);
|
|
3303
|
+
this.http2 = new RawHttp2Handlers(eventBus);
|
|
3304
|
+
}
|
|
3305
|
+
handleDnsResolve(args) {
|
|
3306
|
+
return this.dnsHttp.handleDnsResolve(args);
|
|
3307
|
+
}
|
|
3308
|
+
handleDnsReverse(args) {
|
|
3309
|
+
return this.dnsHttp.handleDnsReverse(args);
|
|
3310
|
+
}
|
|
3311
|
+
handleHttpRequestBuild(args) {
|
|
3312
|
+
return this.dnsHttp.handleHttpRequestBuild(args);
|
|
3313
|
+
}
|
|
3314
|
+
handleHttpPlainRequest(args) {
|
|
3315
|
+
return this.dnsHttp.handleHttpPlainRequest(args);
|
|
3316
|
+
}
|
|
3317
|
+
handleHttp2Probe(args) {
|
|
3318
|
+
return this.http2.handleHttp2Probe(args);
|
|
3319
|
+
}
|
|
3320
|
+
handleHttp2FrameBuild(args) {
|
|
3321
|
+
return this.http2.handleHttp2FrameBuild(args);
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
//#endregion
|
|
3325
|
+
//#region src/server/domains/network/handlers/tls-bot-handlers.ts
|
|
3326
|
+
const GREASE_HEX = new Set([
|
|
3327
|
+
"0a0a",
|
|
3328
|
+
"1a1a",
|
|
3329
|
+
"2a2a",
|
|
3330
|
+
"3a3a",
|
|
3331
|
+
"4a4a",
|
|
3332
|
+
"5a5a",
|
|
3333
|
+
"6a6a",
|
|
3334
|
+
"7a7a",
|
|
3335
|
+
"8a8a",
|
|
3336
|
+
"9a9a",
|
|
3337
|
+
"aaaa",
|
|
3338
|
+
"baba",
|
|
3339
|
+
"caca",
|
|
3340
|
+
"dada",
|
|
3341
|
+
"eaea",
|
|
3342
|
+
"fafa"
|
|
3343
|
+
]);
|
|
3344
|
+
function isGrease(hex) {
|
|
3345
|
+
return GREASE_HEX.has(hex.replace("0x", "").toLowerCase().padStart(4, "0"));
|
|
3346
|
+
}
|
|
3347
|
+
function sha256trunc12(input) {
|
|
3348
|
+
return createHash("sha256").update(input).digest("hex").substring(0, 12);
|
|
3349
|
+
}
|
|
3350
|
+
function toHex4(val) {
|
|
3351
|
+
return val.replace("0x", "").toLowerCase().padStart(4, "0");
|
|
3352
|
+
}
|
|
3353
|
+
const TLS_VERSION_MAP = {
|
|
3354
|
+
"0304": "13",
|
|
3355
|
+
"0303": "12",
|
|
3356
|
+
"0302": "11",
|
|
3357
|
+
"0301": "10",
|
|
3358
|
+
"0300": "s3",
|
|
3359
|
+
"0002": "s2",
|
|
3360
|
+
feff: "d1",
|
|
3361
|
+
fefd: "d2",
|
|
3362
|
+
fefc: "d3"
|
|
3363
|
+
};
|
|
3364
|
+
function encodeTlsVersion(versionHex) {
|
|
3365
|
+
return TLS_VERSION_MAP[versionHex.toLowerCase()] ?? "00";
|
|
3366
|
+
}
|
|
3367
|
+
function encodeAlpn(alpn) {
|
|
3368
|
+
if (!alpn || alpn.length === 0) return "00";
|
|
3369
|
+
const first = alpn[0];
|
|
3370
|
+
const last = alpn[alpn.length - 1];
|
|
3371
|
+
const isFirstAlphaNum = /[0-9a-zA-Z]/.test(first);
|
|
3372
|
+
const isLastAlphaNum = /[0-9a-zA-Z]/.test(last);
|
|
3373
|
+
if (isFirstAlphaNum && isLastAlphaNum) return `${first}${last}`;
|
|
3374
|
+
const hex = Buffer.from(alpn, "utf8").toString("hex");
|
|
3375
|
+
return `${hex[0] ?? "0"}${hex[hex.length - 1] ?? "0"}`;
|
|
3376
|
+
}
|
|
3377
|
+
function computeTlsFingerprint(opts) {
|
|
3378
|
+
const { protocol, tlsVersion, hasSni, ciphers, extensions, signatureAlgorithms, alpn } = opts;
|
|
3379
|
+
const protoChar = protocol === "quic" ? "q" : protocol === "dtls" ? "d" : "t";
|
|
3380
|
+
const filteredVersions = [tlsVersion].map(toHex4).filter((v) => !isGrease(v) && v !== "0303");
|
|
3381
|
+
const sorted = (tlsVersion.length > 0 ? filteredVersions : ["0303"]).toSorted();
|
|
3382
|
+
const versionStr = encodeTlsVersion(sorted.length > 0 ? sorted[sorted.length - 1] : "0303");
|
|
3383
|
+
const sniChar = hasSni ? "d" : "i";
|
|
3384
|
+
const filteredCiphers = ciphers.map(toHex4).filter((c) => !isGrease(c));
|
|
3385
|
+
const filteredExts = extensions.map(toHex4).filter((e) => !isGrease(e));
|
|
3386
|
+
const a = `${protoChar}${versionStr}${sniChar}${String(Math.min(filteredCiphers.length, 99)).padStart(2, "0")}${String(Math.min(filteredExts.length, 99)).padStart(2, "0")}${encodeAlpn(alpn)}`;
|
|
3387
|
+
const cipherStr = filteredCiphers.toSorted().join(",");
|
|
3388
|
+
const cipherHash = filteredCiphers.length > 0 ? sha256trunc12(cipherStr) : "000000000000";
|
|
3389
|
+
const extsForHash = filteredExts.filter((e) => e !== "0000" && e !== "0010").toSorted();
|
|
3390
|
+
const sigHex = signatureAlgorithms.map(toHex4);
|
|
3391
|
+
let extInput;
|
|
3392
|
+
if (sigHex.length > 0) extInput = `${extsForHash.join(",")}_${sigHex.join(",")}`;
|
|
3393
|
+
else extInput = extsForHash.join(",");
|
|
3394
|
+
return {
|
|
3395
|
+
tls: `${a}_${cipherHash}_${extsForHash.length > 0 || sigHex.length > 0 ? sha256trunc12(extInput) : "000000000000"}`,
|
|
3396
|
+
tls_raw: `${a}_${filteredCiphers.join(",")}_${filteredExts.join(",")}_${sigHex.join(",")}`
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
function computeHttpFingerprint(method, headers, httpVersion, cookieHeader, acceptLanguage) {
|
|
3400
|
+
const methodUpper = method.toUpperCase();
|
|
3401
|
+
const methodCode = {
|
|
3402
|
+
GET: "ge",
|
|
3403
|
+
POST: "po",
|
|
3404
|
+
PUT: "pu",
|
|
3405
|
+
DELETE: "de",
|
|
3406
|
+
HEAD: "he",
|
|
3407
|
+
PATCH: "pa",
|
|
3408
|
+
OPTIONS: "ot"
|
|
3409
|
+
}[methodUpper] ?? methodUpper.toLowerCase().substring(0, 2).padEnd(2, methodUpper.charAt(0).toLowerCase());
|
|
3410
|
+
const normalizedHttpVersion = typeof httpVersion === "string" ? httpVersion.trim().toLowerCase() : "";
|
|
3411
|
+
const versionStr = normalizedHttpVersion === "2" || normalizedHttpVersion === "2.0" || normalizedHttpVersion === "h2" || normalizedHttpVersion === "http/2" ? "20" : normalizedHttpVersion === "3" || normalizedHttpVersion === "3.0" || normalizedHttpVersion === "h3" || normalizedHttpVersion === "http/3" ? "30" : normalizedHttpVersion === "1.0" || normalizedHttpVersion === "http/1.0" ? "10" : normalizedHttpVersion === "1.1" || normalizedHttpVersion === "http/1.1" ? "11" : "00";
|
|
3412
|
+
const lowerHeaders = headers.map((h) => h.toLowerCase());
|
|
3413
|
+
const hasCookie = lowerHeaders.includes("cookie") ? "c" : "n";
|
|
3414
|
+
const hasReferer = lowerHeaders.includes("referer") ? "r" : "n";
|
|
3415
|
+
const nonCookieRefererHeaders = lowerHeaders.filter((h) => h !== "cookie" && h !== "referer");
|
|
3416
|
+
const numHeaders = String(Math.min(nonCookieRefererHeaders.length, 99)).padStart(2, "0");
|
|
3417
|
+
let langStr = "0000";
|
|
3418
|
+
if (acceptLanguage && acceptLanguage.length > 0) langStr = (acceptLanguage.split(",")[0] ?? "").replace(/[-;]/g, "").toLowerCase().trim().padEnd(4, "0").substring(0, 4);
|
|
3419
|
+
const a = `${methodCode}${versionStr}${hasCookie}${hasReferer}${numHeaders}${langStr}`;
|
|
3420
|
+
const sortedHeaders = nonCookieRefererHeaders.filter((h) => !h.startsWith(":")).toSorted();
|
|
3421
|
+
const headerHash = sortedHeaders.length > 0 ? sha256trunc12(sortedHeaders.join(",")) : "000000000000";
|
|
3422
|
+
let cookieNamesHash = "000000000000";
|
|
3423
|
+
let cookieValuesHash = "000000000000";
|
|
3424
|
+
if (hasCookie === "c" && cookieHeader) {
|
|
3425
|
+
const cookiePairs = cookieHeader.split(";").map((c) => c.trim()).filter(Boolean);
|
|
3426
|
+
const cookieNames = cookiePairs.map((c) => c.split("=")[0]?.trim() ?? "").filter(Boolean).toSorted();
|
|
3427
|
+
cookieNamesHash = cookieNames.length > 0 ? sha256trunc12(cookieNames.join(",")) : "000000000000";
|
|
3428
|
+
const sortedValues = cookiePairs.map((c) => {
|
|
3429
|
+
const eqIdx = c.indexOf("=");
|
|
3430
|
+
return {
|
|
3431
|
+
name: eqIdx >= 0 ? c.substring(0, eqIdx).trim() : c.trim(),
|
|
3432
|
+
pair: c
|
|
3433
|
+
};
|
|
3434
|
+
}).toSorted((x, y) => x.name.localeCompare(y.name)).map((p) => p.pair);
|
|
3435
|
+
cookieValuesHash = sortedValues.length > 0 ? sha256trunc12(sortedValues.join(",")) : "000000000000";
|
|
3436
|
+
}
|
|
3437
|
+
return { http: `${a}_${headerHash}_${cookieNamesHash}_${cookieValuesHash}` };
|
|
3438
|
+
}
|
|
3439
|
+
function normalizeObservedHttpVersion(httpVersion) {
|
|
3440
|
+
if (typeof httpVersion !== "string") return void 0;
|
|
3441
|
+
const normalized = httpVersion.trim().toLowerCase();
|
|
3442
|
+
if (normalized === "1.0" || normalized === "http/1.0") return "1.0";
|
|
3443
|
+
if (normalized === "1.1" || normalized === "http/1.1") return "1.1";
|
|
3444
|
+
if (normalized === "2" || normalized === "2.0" || normalized === "http/2" || normalized === "h2") return "h2";
|
|
3445
|
+
if (normalized === "3" || normalized === "3.0" || normalized === "http/3" || normalized === "h3") return "h3";
|
|
3446
|
+
}
|
|
3447
|
+
function detectBotSignals(ua, headerNames, tlsSignals) {
|
|
3448
|
+
const signals = [];
|
|
3449
|
+
let score = 0;
|
|
3450
|
+
if (!ua || ua.length === 0) {
|
|
3451
|
+
signals.push("missing-user-agent");
|
|
3452
|
+
score += .3;
|
|
3453
|
+
} else {
|
|
3454
|
+
if (/bot|crawler|spider|headless|selenium|puppeteer|playwright|phantom|curl|wget|python|java|go-http|httpclient|okhttp|requests\/|aiohttp|axios|node-fetch|undici/i.test(ua)) {
|
|
3455
|
+
signals.push(`bot-ua: ${ua.substring(0, 40)}`);
|
|
3456
|
+
score += .5;
|
|
3457
|
+
}
|
|
3458
|
+
if (/headless/i.test(ua)) {
|
|
3459
|
+
signals.push("headless-browser");
|
|
3460
|
+
score += .4;
|
|
3461
|
+
}
|
|
3462
|
+
if (ua.length < 30 && !/bot|curl|wget|python/i.test(ua)) {
|
|
3463
|
+
signals.push("suspiciously-short-ua");
|
|
3464
|
+
score += .2;
|
|
3216
3465
|
}
|
|
3217
3466
|
}
|
|
3218
|
-
|
|
3467
|
+
const lowerHeaders = headerNames.map((h) => h.toLowerCase());
|
|
3468
|
+
if (!lowerHeaders.includes("accept")) {
|
|
3469
|
+
signals.push("missing-accept-header");
|
|
3470
|
+
score += .15;
|
|
3471
|
+
}
|
|
3472
|
+
if (!lowerHeaders.includes("accept-language")) {
|
|
3473
|
+
signals.push("missing-accept-language");
|
|
3474
|
+
score += .1;
|
|
3475
|
+
}
|
|
3476
|
+
if (!lowerHeaders.includes("accept-encoding")) {
|
|
3477
|
+
signals.push("missing-accept-encoding");
|
|
3478
|
+
score += .1;
|
|
3479
|
+
}
|
|
3480
|
+
const headerCount = headerNames.length;
|
|
3481
|
+
if (headerCount < 4) {
|
|
3482
|
+
signals.push(`suspicious-few-headers: ${headerCount}`);
|
|
3483
|
+
score += .2;
|
|
3484
|
+
}
|
|
3485
|
+
if (tlsSignals) {
|
|
3486
|
+
if (tlsSignals.cipherCount <= 2) {
|
|
3487
|
+
signals.push(`anomalous-cipher-count: ${tlsSignals.cipherCount} (real browsers typically 5-15)`);
|
|
3488
|
+
score += .3;
|
|
3489
|
+
}
|
|
3490
|
+
if (tlsSignals.extensionCount < 5) {
|
|
3491
|
+
signals.push(`few-tls-extensions: ${tlsSignals.extensionCount} (real browsers 10-25+)`);
|
|
3492
|
+
score += .2;
|
|
3493
|
+
}
|
|
3494
|
+
if (/\bTLS\s*1\.[01]\b|\b1\.0\b|\b1\.1\b|\bSSL/i.test(tlsSignals.tlsVersion)) {
|
|
3495
|
+
signals.push(`outdated-tls-version: ${tlsSignals.tlsVersion}`);
|
|
3496
|
+
score += .25;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
const expectedBrowserOrder = [
|
|
3500
|
+
"host",
|
|
3501
|
+
"connection",
|
|
3502
|
+
"cache-control",
|
|
3503
|
+
"sec-ch-ua",
|
|
3504
|
+
"sec-ch-ua-mobile",
|
|
3505
|
+
"sec-ch-ua-platform",
|
|
3506
|
+
"upgrade-insecure-requests",
|
|
3507
|
+
"user-agent",
|
|
3508
|
+
"accept",
|
|
3509
|
+
"sec-fetch-site",
|
|
3510
|
+
"sec-fetch-mode",
|
|
3511
|
+
"sec-fetch-user",
|
|
3512
|
+
"sec-fetch-dest",
|
|
3513
|
+
"referer",
|
|
3514
|
+
"accept-encoding",
|
|
3515
|
+
"accept-language"
|
|
3516
|
+
];
|
|
3517
|
+
if (lowerHeaders.length >= 5) {
|
|
3518
|
+
if (lowerHeaders.slice(0, 5).filter((h, i) => h === expectedBrowserOrder[i]).length === 0) {
|
|
3519
|
+
signals.push("header-order-does-not-match-known-browser");
|
|
3520
|
+
score += .1;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
return {
|
|
3524
|
+
score: Math.min(score, 1),
|
|
3525
|
+
signals
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
var TlsBotHandlers = class {
|
|
3529
|
+
consoleMonitor;
|
|
3530
|
+
constructor(deps) {
|
|
3531
|
+
this.consoleMonitor = deps.consoleMonitor;
|
|
3532
|
+
}
|
|
3533
|
+
async handleNetworkTlsFingerprint(args) {
|
|
3534
|
+
const mode = args["mode"];
|
|
3535
|
+
const includeAnalysis = args["includeAnalysis"] !== false;
|
|
3536
|
+
const validModes = [
|
|
3537
|
+
"compute_tls",
|
|
3538
|
+
"compute_http",
|
|
3539
|
+
"analyze_request"
|
|
3540
|
+
];
|
|
3541
|
+
if (!mode || !validModes.includes(mode)) return R.fail(`Invalid mode: "${mode}". Expected one of: ${validModes.join(", ")}`).json();
|
|
3219
3542
|
try {
|
|
3220
|
-
if (
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3543
|
+
if (mode === "compute_tls") {
|
|
3544
|
+
const tlsVersions = args["tlsVersions"] || [];
|
|
3545
|
+
const ciphers = args["ciphers"] || [];
|
|
3546
|
+
const extensions = args["extensions"] || [];
|
|
3547
|
+
const signatureAlgorithms = args["signatureAlgorithms"] || [];
|
|
3548
|
+
const protocol = args["protocol"] === "quic" ? "quic" : args["protocol"] === "dtls" ? "dtls" : "tls";
|
|
3549
|
+
const sni = args["sni"] !== false;
|
|
3550
|
+
const alpn = args["alpn"] || "";
|
|
3551
|
+
if (ciphers.length === 0) return R.fail("ciphers array is required for compute_tls mode").json();
|
|
3552
|
+
const sortedVersions = tlsVersions.map(toHex4).filter((v) => !isGrease(v)).toSorted();
|
|
3553
|
+
const tlsVersion = sortedVersions.length > 0 ? sortedVersions[sortedVersions.length - 1] : "0303";
|
|
3554
|
+
const { tls, tls_raw } = computeTlsFingerprint({
|
|
3555
|
+
protocol,
|
|
3556
|
+
tlsVersion,
|
|
3557
|
+
hasSni: sni,
|
|
3558
|
+
ciphers,
|
|
3559
|
+
extensions,
|
|
3560
|
+
signatureAlgorithms,
|
|
3561
|
+
alpn
|
|
3562
|
+
});
|
|
3563
|
+
const result = {
|
|
3564
|
+
success: true,
|
|
3565
|
+
mode: "tls",
|
|
3566
|
+
tls,
|
|
3567
|
+
tls_raw
|
|
3568
|
+
};
|
|
3569
|
+
if (includeAnalysis) {
|
|
3570
|
+
const filteredCiphers = ciphers.map(toHex4).filter((c) => !isGrease(c));
|
|
3571
|
+
const filteredExts = extensions.map(toHex4).filter((e) => !isGrease(e));
|
|
3572
|
+
result.analysis = {
|
|
3573
|
+
protocol: protocol.toUpperCase(),
|
|
3574
|
+
tlsVersion,
|
|
3575
|
+
sni,
|
|
3576
|
+
cipherCount: filteredCiphers.length,
|
|
3577
|
+
extensionCount: filteredExts.length,
|
|
3578
|
+
signatureAlgorithmCount: signatureAlgorithms.length,
|
|
3579
|
+
alpn: alpn || "(none)",
|
|
3580
|
+
sortedCiphers: filteredCiphers.toSorted(),
|
|
3581
|
+
sortedExtensions: filteredExts.filter((e) => e !== "0000" && e !== "0010").toSorted()
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
return R.ok().merge(result).json();
|
|
3585
|
+
}
|
|
3586
|
+
if (mode === "compute_http") {
|
|
3587
|
+
const headers = args["httpHeaders"] || [];
|
|
3588
|
+
const ua = args["userAgent"] || "";
|
|
3589
|
+
const method = args["httpMethod"] || "GET";
|
|
3590
|
+
const httpVersion = args["httpVersion"] || "1.1";
|
|
3591
|
+
const cookieHeader = args["cookieHeader"] || "";
|
|
3592
|
+
const acceptLanguage = args["acceptLanguage"] || "";
|
|
3593
|
+
if (headers.length === 0) return R.fail("httpHeaders array is required for compute_http mode").json();
|
|
3594
|
+
const { http } = computeHttpFingerprint(method, headers, httpVersion, cookieHeader, acceptLanguage);
|
|
3595
|
+
const result = {
|
|
3596
|
+
success: true,
|
|
3597
|
+
mode: "http",
|
|
3598
|
+
http
|
|
3599
|
+
};
|
|
3600
|
+
if (includeAnalysis) {
|
|
3601
|
+
const lowerHeaders = headers.map((h) => h.toLowerCase());
|
|
3602
|
+
result.analysis = {
|
|
3603
|
+
method,
|
|
3604
|
+
httpVersion,
|
|
3605
|
+
headerCount: headers.length,
|
|
3606
|
+
nonCookieRefererHeaders: lowerHeaders.filter((h) => h !== "cookie" && h !== "referer").length,
|
|
3607
|
+
hasCookie: lowerHeaders.includes("cookie"),
|
|
3608
|
+
hasAcceptLanguage: lowerHeaders.includes("accept-language"),
|
|
3609
|
+
sortedHeaders: lowerHeaders.filter((h) => h !== "cookie" && h !== "referer" && !h.startsWith(":")).toSorted(),
|
|
3610
|
+
userAgentLength: ua.length
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
return R.ok().merge(result).json();
|
|
3614
|
+
}
|
|
3615
|
+
const requestId = args["requestId"];
|
|
3616
|
+
if (!requestId) return R.fail("requestId is required for analyze_request mode").json();
|
|
3617
|
+
const req = this.consoleMonitor.getNetworkRequests().find((r) => r.requestId === requestId);
|
|
3618
|
+
if (!req) return R.fail(`Request ${requestId} not found`).json();
|
|
3619
|
+
const headers = req.headers || {};
|
|
3620
|
+
const headerNames = Object.keys(headers);
|
|
3621
|
+
const ua = headers["user-agent"] || headers["User-Agent"] || "";
|
|
3622
|
+
const method = req.method || "GET";
|
|
3623
|
+
const cookieHeader = headers["cookie"] || headers["Cookie"] || "";
|
|
3624
|
+
const acceptLanguage = headers["accept-language"] || headers["Accept-Language"] || "";
|
|
3625
|
+
const httpVersion = normalizeObservedHttpVersion(req.httpVersion);
|
|
3626
|
+
const { http } = computeHttpFingerprint(method, headerNames, httpVersion, cookieHeader, acceptLanguage);
|
|
3627
|
+
const secDetails = req["securityDetails"];
|
|
3628
|
+
const tlsSignalsForBot = secDetails && typeof secDetails === "object" ? {
|
|
3629
|
+
cipherCount: typeof secDetails["cipherCount"] === "number" ? secDetails["cipherCount"] : 5,
|
|
3630
|
+
extensionCount: typeof secDetails["extensionCount"] === "number" ? secDetails["extensionCount"] : 10,
|
|
3631
|
+
tlsVersion: typeof secDetails["protocol"] === "string" ? secDetails["protocol"] : ""
|
|
3632
|
+
} : void 0;
|
|
3633
|
+
const result = {
|
|
3634
|
+
success: true,
|
|
3635
|
+
mode: "analyze_request",
|
|
3636
|
+
requestId,
|
|
3637
|
+
url: req.url,
|
|
3638
|
+
method,
|
|
3639
|
+
httpVersion: httpVersion ?? "unknown",
|
|
3640
|
+
http
|
|
3641
|
+
};
|
|
3642
|
+
const analysis = {
|
|
3643
|
+
requestId,
|
|
3644
|
+
url: req.url,
|
|
3645
|
+
method,
|
|
3646
|
+
httpVersion: httpVersion ?? "unknown",
|
|
3647
|
+
http,
|
|
3648
|
+
headerCount: headerNames.length,
|
|
3649
|
+
headerOrder: headerNames.join(", "),
|
|
3650
|
+
userAgent: ua.length > 80 ? ua.substring(0, 80) + "..." : ua,
|
|
3651
|
+
securityHeaders: {
|
|
3652
|
+
hasCSP: void 0,
|
|
3653
|
+
hasHSTS: void 0,
|
|
3654
|
+
hasCORS: void 0
|
|
3655
|
+
},
|
|
3656
|
+
botSignals: detectBotSignals(ua, headerNames, tlsSignalsForBot)
|
|
3657
|
+
};
|
|
3658
|
+
if (includeAnalysis) result.analysis = analysis;
|
|
3659
|
+
return R.ok().merge(result).json();
|
|
3660
|
+
} catch (error) {
|
|
3661
|
+
return R.fail(error instanceof Error ? error.message : String(error)).json();
|
|
3235
3662
|
}
|
|
3236
3663
|
}
|
|
3664
|
+
async handleNetworkBotDetectAnalyze(args) {
|
|
3665
|
+
const limit = typeof args["limit"] === "number" ? args["limit"] : BOT_DETECT_LIMIT_DEFAULT;
|
|
3666
|
+
const includeDetails = args["includeDetails"] === true;
|
|
3667
|
+
const requests = this.consoleMonitor.getNetworkRequests();
|
|
3668
|
+
const sample = requests.slice(0, limit);
|
|
3669
|
+
if (sample.length === 0) return R.ok().merge({
|
|
3670
|
+
analyzed: 0,
|
|
3671
|
+
summary: "No captured requests to analyze. Enable network monitoring first."
|
|
3672
|
+
}).json();
|
|
3673
|
+
const signals = [];
|
|
3674
|
+
const details = [];
|
|
3675
|
+
let totalBotScore = 0;
|
|
3676
|
+
const httpFingerprints = /* @__PURE__ */ new Map();
|
|
3677
|
+
for (const req of sample) {
|
|
3678
|
+
const headers = req.headers || {};
|
|
3679
|
+
const headerNames = Object.keys(headers);
|
|
3680
|
+
const ua = headers["user-agent"] || headers["User-Agent"] || "";
|
|
3681
|
+
const url = req.url || "";
|
|
3682
|
+
const method = req.method || "GET";
|
|
3683
|
+
const cookieHeader = headers["cookie"] || headers["Cookie"] || "";
|
|
3684
|
+
const acceptLanguage = headers["accept-language"] || headers["Accept-Language"] || "";
|
|
3685
|
+
const httpVersion = normalizeObservedHttpVersion(req.httpVersion);
|
|
3686
|
+
const secDetails = req["securityDetails"];
|
|
3687
|
+
const reqSignals = detectBotSignals(ua, headerNames, secDetails && typeof secDetails === "object" ? {
|
|
3688
|
+
cipherCount: typeof secDetails["cipherCount"] === "number" ? secDetails["cipherCount"] : 5,
|
|
3689
|
+
extensionCount: typeof secDetails["extensionCount"] === "number" ? secDetails["extensionCount"] : 10,
|
|
3690
|
+
tlsVersion: typeof secDetails["protocol"] === "string" ? secDetails["protocol"] : ""
|
|
3691
|
+
} : void 0);
|
|
3692
|
+
const isApiRequest = /\/api\/|\/v\d+\/|\/graphql/i.test(url);
|
|
3693
|
+
const { http } = computeHttpFingerprint(method, headerNames, httpVersion, cookieHeader, acceptLanguage);
|
|
3694
|
+
httpFingerprints.set(http, (httpFingerprints.get(http) ?? 0) + 1);
|
|
3695
|
+
const reqDetail = {
|
|
3696
|
+
requestId: req.requestId,
|
|
3697
|
+
url: url.length > 100 ? url.substring(0, 100) + "..." : url,
|
|
3698
|
+
method,
|
|
3699
|
+
http,
|
|
3700
|
+
botScore: reqSignals.score,
|
|
3701
|
+
signals: reqSignals.signals
|
|
3702
|
+
};
|
|
3703
|
+
totalBotScore += reqSignals.score;
|
|
3704
|
+
if (reqSignals.score > .5) signals.push(`Request ${req.requestId}: ${reqSignals.signals.join(", ")}`);
|
|
3705
|
+
if (isApiRequest) reqDetail.apiPattern = true;
|
|
3706
|
+
if (includeDetails) details.push(reqDetail);
|
|
3707
|
+
}
|
|
3708
|
+
const avgBotScore = sample.length > 0 ? totalBotScore / sample.length : 0;
|
|
3709
|
+
const uniqueFingerprints = httpFingerprints.size;
|
|
3710
|
+
const fingerprintDiversity = uniqueFingerprints / sample.length;
|
|
3711
|
+
const diversitySignals = [];
|
|
3712
|
+
if (fingerprintDiversity > .8) diversitySignals.push(`High fingerprint diversity (${uniqueFingerprints} unique HTTP fingerprints in ${sample.length} requests) — may indicate multiple clients or rotation`);
|
|
3713
|
+
if (fingerprintDiversity === 1 && sample.length > 5) diversitySignals.push(`Every request has a unique HTTP fingerprint — likely automated tool rotating headers`);
|
|
3714
|
+
return R.ok().merge({
|
|
3715
|
+
analyzed: sample.length,
|
|
3716
|
+
totalRequests: requests.length,
|
|
3717
|
+
averageBotScore: Math.round(avgBotScore * 100) / 100,
|
|
3718
|
+
suspiciousRequests: signals.length,
|
|
3719
|
+
httpFingerprintSummary: {
|
|
3720
|
+
uniqueFingerprints,
|
|
3721
|
+
diversity: Math.round(fingerprintDiversity * 100) / 100,
|
|
3722
|
+
topFingerprints: [...httpFingerprints.entries()].toSorted((a, b) => b[1] - a[1]).slice(0, 5).map(([fp, count]) => ({
|
|
3723
|
+
http_fingerprint: fp,
|
|
3724
|
+
count
|
|
3725
|
+
}))
|
|
3726
|
+
},
|
|
3727
|
+
signals: signals.slice(0, 20),
|
|
3728
|
+
...diversitySignals.length > 0 ? { diversitySignals } : {},
|
|
3729
|
+
details: includeDetails ? details : void 0,
|
|
3730
|
+
recommendations: avgBotScore > .5 ? [
|
|
3731
|
+
"High bot-like signal detected. Consider TLS fingerprint rotation.",
|
|
3732
|
+
"Review User-Agent consistency across requests.",
|
|
3733
|
+
"Check header ordering matches real browser behavior.",
|
|
3734
|
+
"HTTP fingerprint diversity can distinguish botnets from real users."
|
|
3735
|
+
] : fingerprintDiversity > .8 ? ["Traffic appears human but fingerprint diversity is high — investigate further."] : ["Traffic appears to follow normal browser patterns."]
|
|
3736
|
+
}).json();
|
|
3737
|
+
}
|
|
3237
3738
|
};
|
|
3238
3739
|
//#endregion
|
|
3239
3740
|
//#region src/server/domains/network/handlers.impl.ts
|
|
@@ -3245,11 +3746,12 @@ var AdvancedToolHandlers = class {
|
|
|
3245
3746
|
detailedDataManager = DetailedDataManager.getInstance();
|
|
3246
3747
|
core;
|
|
3247
3748
|
perf;
|
|
3248
|
-
|
|
3749
|
+
consoleHandlers;
|
|
3249
3750
|
replay;
|
|
3250
3751
|
intercept;
|
|
3251
3752
|
raw;
|
|
3252
|
-
|
|
3753
|
+
tlsBot;
|
|
3754
|
+
constructor(collector, consoleMonitor, eventBus, getTraceRecorder) {
|
|
3253
3755
|
this.collector = collector;
|
|
3254
3756
|
this.consoleMonitor = consoleMonitor;
|
|
3255
3757
|
this.eventBus = eventBus;
|
|
@@ -3260,15 +3762,17 @@ var AdvancedToolHandlers = class {
|
|
|
3260
3762
|
});
|
|
3261
3763
|
this.perf = new PerformanceHandlers({
|
|
3262
3764
|
collector,
|
|
3263
|
-
getPerformanceMonitor: () => this.getPerformanceMonitor()
|
|
3765
|
+
getPerformanceMonitor: () => this.getPerformanceMonitor(),
|
|
3766
|
+
getTraceRecorder
|
|
3264
3767
|
});
|
|
3265
|
-
this.
|
|
3768
|
+
this.consoleHandlers = new ConsoleHandlers({ consoleMonitor });
|
|
3266
3769
|
this.replay = new ReplayHandlers({ consoleMonitor });
|
|
3267
3770
|
this.intercept = new InterceptHandlers({
|
|
3268
3771
|
consoleMonitor,
|
|
3269
3772
|
eventBus
|
|
3270
3773
|
});
|
|
3271
3774
|
this.raw = new RawHandlers(eventBus);
|
|
3775
|
+
this.tlsBot = new TlsBotHandlers({ consoleMonitor });
|
|
3272
3776
|
}
|
|
3273
3777
|
getPerformanceMonitor() {
|
|
3274
3778
|
if (!this.performanceMonitor) this.performanceMonitor = new PerformanceMonitor(this.collector);
|
|
@@ -3295,24 +3799,24 @@ var AdvancedToolHandlers = class {
|
|
|
3295
3799
|
handleProfilerHeapSamplingStart = (args) => this.perf.handleProfilerHeapSamplingStart(args);
|
|
3296
3800
|
handleProfilerHeapSamplingStop = (args) => this.perf.handleProfilerHeapSamplingStop(args);
|
|
3297
3801
|
handleProfilerHeapSamplingDispatch = (args) => String(args["action"] ?? "") === "stop" ? this.perf.handleProfilerHeapSamplingStop(args) : this.perf.handleProfilerHeapSamplingStart(args);
|
|
3298
|
-
handleConsoleGetExceptions = (args) => this.
|
|
3802
|
+
handleConsoleGetExceptions = (args) => this.consoleHandlers.handleConsoleGetExceptions(args);
|
|
3299
3803
|
handleConsoleInjectDispatch = (args) => {
|
|
3300
3804
|
switch (String(args["type"] ?? "")) {
|
|
3301
|
-
case "xhr": return this.
|
|
3302
|
-
case "fetch": return this.
|
|
3303
|
-
case "function": return this.
|
|
3304
|
-
default: return this.
|
|
3805
|
+
case "xhr": return this.consoleHandlers.handleConsoleInjectXhrInterceptor(args);
|
|
3806
|
+
case "fetch": return this.consoleHandlers.handleConsoleInjectFetchInterceptor(args);
|
|
3807
|
+
case "function": return this.consoleHandlers.handleConsoleInjectFunctionTracer(args);
|
|
3808
|
+
default: return this.consoleHandlers.handleConsoleInjectScriptMonitor(args);
|
|
3305
3809
|
}
|
|
3306
3810
|
};
|
|
3307
3811
|
handleConsoleBuffersDispatch = (args) => {
|
|
3308
|
-
return String(args["action"] ?? "") === "reset" ? this.
|
|
3812
|
+
return String(args["action"] ?? "") === "reset" ? this.consoleHandlers.handleConsoleResetInjectedInterceptors(args) : this.consoleHandlers.handleConsoleClearInjectedBuffers(args);
|
|
3309
3813
|
};
|
|
3310
|
-
handleConsoleInjectScriptMonitor = (args) => this.
|
|
3311
|
-
handleConsoleInjectXhrInterceptor = (args) => this.
|
|
3312
|
-
handleConsoleInjectFetchInterceptor = (args) => this.
|
|
3313
|
-
handleConsoleClearInjectedBuffers = (args) => this.
|
|
3314
|
-
handleConsoleResetInjectedInterceptors = (args) => this.
|
|
3315
|
-
handleConsoleInjectFunctionTracer = (args) => this.
|
|
3814
|
+
handleConsoleInjectScriptMonitor = (args) => this.consoleHandlers.handleConsoleInjectScriptMonitor(args);
|
|
3815
|
+
handleConsoleInjectXhrInterceptor = (args) => this.consoleHandlers.handleConsoleInjectXhrInterceptor(args);
|
|
3816
|
+
handleConsoleInjectFetchInterceptor = (args) => this.consoleHandlers.handleConsoleInjectFetchInterceptor(args);
|
|
3817
|
+
handleConsoleClearInjectedBuffers = (args) => this.consoleHandlers.handleConsoleClearInjectedBuffers(args);
|
|
3818
|
+
handleConsoleResetInjectedInterceptors = (args) => this.consoleHandlers.handleConsoleResetInjectedInterceptors(args);
|
|
3819
|
+
handleConsoleInjectFunctionTracer = (args) => this.consoleHandlers.handleConsoleInjectFunctionTracer(args);
|
|
3316
3820
|
handleNetworkExtractAuth = (args) => this.replay.handleNetworkExtractAuth(args);
|
|
3317
3821
|
handleNetworkExportHar = (args) => this.replay.handleNetworkExportHar(args);
|
|
3318
3822
|
handleNetworkReplayRequest = (args) => this.replay.handleNetworkReplayRequest(args);
|
|
@@ -3341,6 +3845,8 @@ var AdvancedToolHandlers = class {
|
|
|
3341
3845
|
handleHttp2Probe = (args) => this.raw.handleHttp2Probe(args);
|
|
3342
3846
|
handleHttp2FrameBuild = (args) => this.raw.handleHttp2FrameBuild(args);
|
|
3343
3847
|
handleNetworkRttMeasure = (args) => this.raw.handleNetworkRttMeasure(args);
|
|
3848
|
+
handleNetworkTlsFingerprint = (args) => this.tlsBot.handleNetworkTlsFingerprint(args);
|
|
3849
|
+
handleNetworkBotDetectAnalyze = (args) => this.tlsBot.handleNetworkBotDetectAnalyze(args);
|
|
3344
3850
|
};
|
|
3345
3851
|
//#endregion
|
|
3346
3852
|
export { AdvancedToolHandlers };
|