@jshookmcp/jshook 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-BNk-EoBt.mjs} +3 -3
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-Cq8q01kp.mjs} +5 -5
- package/dist/ConsoleMonitor-CPVQW1Y-.mjs +2201 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-BNPxu0RH.mjs} +1 -1
- package/dist/DetailedDataManager-BQQcxh64.mjs +217 -0
- package/dist/EventBus-DgPmwpeu.mjs +141 -0
- package/dist/EvidenceGraphBridge-SFesNera.mjs +153 -0
- package/dist/{ExtensionManager-CZ6IveoV.mjs → ExtensionManager-CWYgw0YW.mjs} +13 -6
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-gzWtkKuf.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-B9gZCdFP.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-BLDH0dCv.mjs} +4 -4
- package/dist/HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs +639 -0
- package/dist/InstrumentationSession-CvPC7Jwy.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CbVdCIJF.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-BsDZbLYm.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-Bcpml6II.mjs} +44 -18
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-dZtA1ZGn.mjs} +14 -53
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-B-FjA2mJ.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-D1lzJ_VG.mjs} +2 -2
- package/dist/PageController-Bqm2kZ_X.mjs +417 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-BOhyVsjx.mjs} +4 -4
- package/dist/PrerequisiteError-Dl33Svkz.mjs +20 -0
- package/dist/ResponseBuilder-D3iFYx2N.mjs +143 -0
- package/dist/ReverseEvidenceGraph-Dlsk94LC.mjs +269 -0
- package/dist/ScriptManager-aHHq0X7U.mjs +3000 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-CqdIFlQl.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-DhFaPvRO.mjs} +3 -3
- package/dist/ToolCatalog-C0JGZoOm.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-oC7aPrkv.mjs +45 -0
- package/dist/ToolRegistry-BjaF4oNz.mjs +131 -0
- package/dist/ToolRouter.policy-BWV67ZK-.mjs +304 -0
- package/dist/TraceRecorder-DgxyVbdQ.mjs +519 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-CePkipZY.mjs} +1 -1
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-BvKs-gxc.mjs} +2 -2
- package/dist/WorkflowEngine-CuvkZtWu.mjs +598 -0
- package/dist/analysis-CL9uACt9.mjs +463 -0
- package/dist/antidebug-CqDTB_uk.mjs +1081 -0
- package/dist/artifactRetention-CFEprwPw.mjs +591 -0
- package/dist/artifacts-Bk2-_uPq.mjs +59 -0
- package/dist/betterSqlite3-0pqusHHH.mjs +74 -0
- package/dist/binary-instrument-CXfpx6fT.mjs +979 -0
- package/dist/bind-helpers-xFfRF-qm.mjs +22 -0
- package/dist/boringssl-inspector-BH2D3VKc.mjs +180 -0
- package/dist/browser-BpOr5PEx.mjs +4082 -0
- package/dist/concurrency-Bt0yv1kJ.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-B0OANIBL.mjs} +88 -46
- package/dist/coordination-qUbyF8KU.mjs +259 -0
- package/dist/debugger-gnKxRSN0.mjs +1271 -0
- package/dist/definitions-6M-eejaT.mjs +53 -0
- package/dist/definitions-B18eyf0B.mjs +18 -0
- package/dist/definitions-B3QdlrHv.mjs +34 -0
- package/dist/definitions-B4rAvHNZ.mjs +63 -0
- package/dist/definitions-BB_4jnmy.mjs +37 -0
- package/dist/definitions-BMfYXoNC.mjs +43 -0
- package/dist/definitions-Beid2EB3.mjs +27 -0
- package/dist/definitions-C1UvM5Iy.mjs +126 -0
- package/dist/definitions-CXEI7QC72.mjs +216 -0
- package/dist/definitions-C_4r7Fo-2.mjs +14 -0
- package/dist/definitions-CkFDALoa.mjs +26 -0
- package/dist/definitions-Cke7zEb8.mjs +94 -0
- package/dist/definitions-ClJLzsJQ.mjs +25 -0
- package/dist/definitions-Cq-zroAU.mjs +28 -0
- package/dist/definitions-Cy3Sl6gV.mjs +34 -0
- package/dist/definitions-D3VsGcvz.mjs +47 -0
- package/dist/definitions-DVGfrn7y.mjs +96 -0
- package/dist/definitions-LKpC3-nL.mjs +9 -0
- package/dist/definitions-bAhHQJq9.mjs +359 -0
- package/dist/encoding-Bvz5jLRv.mjs +1065 -0
- package/dist/evidence-graph-bridge-C_fv9PuC.mjs +135 -0
- package/dist/{factory-CibqTNC8.mjs → factory-DxlGh9Xf.mjs} +37 -52
- package/dist/graphql-DYWzJ29s.mjs +1026 -0
- package/dist/handlers-9sAbfIg-.mjs +2552 -0
- package/dist/handlers-Bl8zkwz1.mjs +2716 -0
- package/dist/handlers-C67ktuRN.mjs +710 -0
- package/dist/handlers-C87g8oCe.mjs +276 -0
- package/dist/handlers-CTsDAO6p.mjs +681 -0
- package/dist/handlers-Cgyg6c0U.mjs +645 -0
- package/dist/handlers-D6j6yka7.mjs +2124 -0
- package/dist/handlers-DdFzXLvF.mjs +446 -0
- package/dist/handlers-DeLOCd5m.mjs +799 -0
- package/dist/handlers-DlCJN4Td.mjs +757 -0
- package/dist/handlers-DxGIq15_2.mjs +917 -0
- package/dist/handlers-U6L4xhuF.mjs +585 -0
- package/dist/handlers-tB9Mp9ZK.mjs +84 -0
- package/dist/handlers-tiy7EIBp.mjs +572 -0
- package/dist/handlers.impl-DS0d9fUw.mjs +761 -0
- package/dist/hooks-CzCWByww.mjs +898 -0
- package/dist/index.mjs +384 -155
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-P7ePRXQC.mjs +830 -0
- package/dist/manifest-2ToTpjv8.mjs +106 -0
- package/dist/manifest-3g71z6Bg.mjs +79 -0
- package/dist/manifest-82baTv4U.mjs +45 -0
- package/dist/manifest-B3QVVeBS.mjs +82 -0
- package/dist/manifest-BB2J8IMJ.mjs +149 -0
- package/dist/manifest-BKbgbSiY.mjs +60 -0
- package/dist/manifest-Bcf-TJzH.mjs +848 -0
- package/dist/manifest-BmtZzQiQ2.mjs +45 -0
- package/dist/manifest-Bnd7kqEY.mjs +55 -0
- package/dist/manifest-BqQX6OQC2.mjs +65 -0
- package/dist/manifest-BqrQ4Tpj.mjs +81 -0
- package/dist/manifest-Br4RPFt5.mjs +370 -0
- package/dist/manifest-C5qDjysN.mjs +107 -0
- package/dist/manifest-C9RT5nk32.mjs +34 -0
- package/dist/manifest-CAhOuvSl.mjs +204 -0
- package/dist/manifest-CBYWCUBJ.mjs +51 -0
- package/dist/manifest-CFADCRa1.mjs +37 -0
- package/dist/manifest-CQVhavRF.mjs +114 -0
- package/dist/manifest-CT7zZBV1.mjs +48 -0
- package/dist/manifest-CV12bcrF.mjs +121 -0
- package/dist/manifest-CXsRWjjI.mjs +224 -0
- package/dist/manifest-CZLUCfG02.mjs +95 -0
- package/dist/manifest-D6phHKFd.mjs +131 -0
- package/dist/manifest-DCyjf4n2.mjs +294 -0
- package/dist/manifest-DHsnKgP6.mjs +60 -0
- package/dist/manifest-Df_dliIe.mjs +55 -0
- package/dist/manifest-Dh8WBmEW.mjs +129 -0
- package/dist/manifest-DhKRAT8_.mjs +92 -0
- package/dist/manifest-DlpTj4ic2.mjs +193 -0
- package/dist/manifest-DrbmZcFl2.mjs +253 -0
- package/dist/manifest-DuwHjUa5.mjs +70 -0
- package/dist/manifest-DzwvxPJX.mjs +38 -0
- package/dist/manifest-NXctwWQq.mjs +68 -0
- package/dist/manifest-Sc_0JQ13.mjs +418 -0
- package/dist/manifest-gZ4s_UtG.mjs +96 -0
- package/dist/manifest-qSleDqdO.mjs +1023 -0
- package/dist/modules-C184v-S9.mjs +11365 -0
- package/dist/mojo-ipc-B_H61Afw.mjs +525 -0
- package/dist/network-671Cw6hV.mjs +3346 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-B1uGmrWZ.mjs} +219 -212
- package/dist/parse-args-BlRjqlkL.mjs +39 -0
- package/dist/platform-WmNn8Sxb.mjs +2070 -0
- package/dist/process-QcbIy5Zq.mjs +1401 -0
- package/dist/proxy-DqNs0bAd.mjs +170 -0
- package/dist/registry-D-6e18lB.mjs +34 -0
- package/dist/response-BQVP-xUn.mjs +28 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-DV-dpHFJ.mjs +586 -0
- package/dist/sourcemap-Dq8ez8vS.mjs +650 -0
- package/dist/ssrf-policy-ZaUfvhq7.mjs +166 -0
- package/dist/streaming-BUQ0VJsg.mjs +725 -0
- package/dist/tool-builder-DCbIC5Eo.mjs +186 -0
- package/dist/transform-CiYJfNX0.mjs +1007 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-DQTnHDs4.mjs +531 -0
- package/dist/workflow-f3xJOcjx.mjs +725 -0
- package/package.json +48 -78
- package/dist/ExtensionManager-DqUSOamB.mjs +0 -2
- package/dist/ToolCatalog-CnwmMIw3.mjs +0 -61483
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-CDe5WPSV.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-Bo4T3bz8.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-CwVLVdDM.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-DVkj9kpI.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-CPhOReNX.mjs} +1 -1
|
@@ -0,0 +1,2552 @@
|
|
|
1
|
+
import { n as asJsonResponse } from "./response-BQVP-xUn.mjs";
|
|
2
|
+
import { a as enableKeyLog, c as parseKeyLog, i as disableKeyLog, l as summarizeKeyLog, n as TLSKeyLogExtractor, o as getKeyLogFilePath, r as decryptPayload, s as lookupSecret } from "./boringssl-inspector-BH2D3VKc.mjs";
|
|
3
|
+
import { a as argString, n as argEnum, o as argStringArray, r as argNumber, t as argBool } from "./parse-args-BlRjqlkL.mjs";
|
|
4
|
+
import { a as isLoopbackHost, s as isPrivateHost } from "./ssrf-policy-ZaUfvhq7.mjs";
|
|
5
|
+
import { X509Certificate, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { Socket, createServer, isIP } from "node:net";
|
|
8
|
+
import { createSocket } from "node:dgram";
|
|
9
|
+
import { checkServerIdentity, connect } from "node:tls";
|
|
10
|
+
//#region src/server/domains/boringssl-inspector/handlers/shared.ts
|
|
11
|
+
function validateNetworkTarget(host) {
|
|
12
|
+
if (isPrivateHost(host) && !isLoopbackHost(host)) return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: `Blocked: target host "${host}" resolves to a private/internal address. SSRF protection applies.`
|
|
15
|
+
};
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const TLS_VERSION_SET = new Set([
|
|
19
|
+
"TLSv1",
|
|
20
|
+
"TLSv1.1",
|
|
21
|
+
"TLSv1.2",
|
|
22
|
+
"TLSv1.3"
|
|
23
|
+
]);
|
|
24
|
+
function errorMessage(error) {
|
|
25
|
+
return error instanceof Error ? error.message : String(error);
|
|
26
|
+
}
|
|
27
|
+
function normalizeSocketServername(servername) {
|
|
28
|
+
return typeof servername === "string" && servername.length > 0 ? servername : null;
|
|
29
|
+
}
|
|
30
|
+
function normalizeAlpnProtocol(protocol) {
|
|
31
|
+
return typeof protocol === "string" && protocol.length > 0 ? protocol : null;
|
|
32
|
+
}
|
|
33
|
+
function applyTlsValidationPolicy(options, allowInvalidCertificates) {
|
|
34
|
+
const next = { ...options };
|
|
35
|
+
Reflect.set(next, "rejectUnauthorized", !allowInvalidCertificates);
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
function isNonEmptyObject(value) {
|
|
39
|
+
return value !== null && typeof value === "object" && Object.keys(value).length > 0;
|
|
40
|
+
}
|
|
41
|
+
function hasPeerCertificate(value) {
|
|
42
|
+
return isNonEmptyObject(value);
|
|
43
|
+
}
|
|
44
|
+
function summarizePeerCertificate(cert, depth) {
|
|
45
|
+
const raw = Buffer.isBuffer(cert.raw) ? cert.raw : null;
|
|
46
|
+
const x509 = raw ? new X509Certificate(raw) : null;
|
|
47
|
+
const subject = x509?.subject ?? null;
|
|
48
|
+
const issuer = x509?.issuer ?? null;
|
|
49
|
+
return {
|
|
50
|
+
depth,
|
|
51
|
+
subject,
|
|
52
|
+
issuer,
|
|
53
|
+
subjectAltName: x509?.subjectAltName ?? cert.subjectaltname ?? null,
|
|
54
|
+
serialNumber: x509?.serialNumber ?? cert.serialNumber ?? null,
|
|
55
|
+
validFrom: x509?.validFrom ?? cert.valid_from ?? null,
|
|
56
|
+
validTo: x509?.validTo ?? cert.valid_to ?? null,
|
|
57
|
+
fingerprint256: x509?.fingerprint256 ?? cert.fingerprint256 ?? null,
|
|
58
|
+
fingerprint512: x509?.fingerprint512 ?? cert.fingerprint512 ?? null,
|
|
59
|
+
rawLength: raw?.length ?? null,
|
|
60
|
+
isCA: x509?.ca ?? cert.ca ?? null,
|
|
61
|
+
selfIssued: subject && issuer ? subject === issuer : null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function buildPeerCertificateChain(peerCertificate) {
|
|
65
|
+
if (!peerCertificate) return [];
|
|
66
|
+
const chain = [];
|
|
67
|
+
const seen = /* @__PURE__ */ new Set();
|
|
68
|
+
let current = peerCertificate;
|
|
69
|
+
let depth = 0;
|
|
70
|
+
while (current && hasPeerCertificate(current)) {
|
|
71
|
+
const summary = summarizePeerCertificate(current, depth);
|
|
72
|
+
const dedupeKey = summary.fingerprint256 ?? `${summary.subject ?? "unknown-subject"}:${summary.serialNumber ?? "unknown-serial"}:${depth}`;
|
|
73
|
+
if (seen.has(dedupeKey)) break;
|
|
74
|
+
seen.add(dedupeKey);
|
|
75
|
+
chain.push(summary);
|
|
76
|
+
if (!("issuerCertificate" in current)) break;
|
|
77
|
+
const issuerCertificate = current.issuerCertificate;
|
|
78
|
+
if (!issuerCertificate || issuerCertificate === current || !hasPeerCertificate(issuerCertificate)) break;
|
|
79
|
+
current = issuerCertificate;
|
|
80
|
+
depth += 1;
|
|
81
|
+
}
|
|
82
|
+
return chain;
|
|
83
|
+
}
|
|
84
|
+
async function loadProbeCaBundle(args) {
|
|
85
|
+
const caPem = argString(args, "caPem") ?? null;
|
|
86
|
+
const caPath = argString(args, "caPath") ?? null;
|
|
87
|
+
if (caPem && caPath) return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: "caPem and caPath are mutually exclusive"
|
|
90
|
+
};
|
|
91
|
+
if (caPem) return {
|
|
92
|
+
ok: true,
|
|
93
|
+
ca: caPem,
|
|
94
|
+
source: "inline",
|
|
95
|
+
path: null,
|
|
96
|
+
bytes: Buffer.byteLength(caPem)
|
|
97
|
+
};
|
|
98
|
+
if (caPath) try {
|
|
99
|
+
const ca = await readFile(caPath, "utf8");
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
ca,
|
|
103
|
+
source: "path",
|
|
104
|
+
path: caPath,
|
|
105
|
+
bytes: Buffer.byteLength(ca)
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: `Failed to read caPath "${caPath}": ${errorMessage(error)}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
ca: void 0,
|
|
116
|
+
source: null,
|
|
117
|
+
path: null,
|
|
118
|
+
bytes: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function makeSessionId(kind) {
|
|
122
|
+
return `${kind}_${randomUUID()}`;
|
|
123
|
+
}
|
|
124
|
+
function serializeSocketAddresses(socket) {
|
|
125
|
+
return {
|
|
126
|
+
localAddress: socket.localAddress ?? null,
|
|
127
|
+
localPort: socket.localPort ?? null,
|
|
128
|
+
remoteAddress: socket.remoteAddress ?? null,
|
|
129
|
+
remotePort: socket.remotePort ?? null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function serializeSessionState(session) {
|
|
133
|
+
return {
|
|
134
|
+
bufferedBytes: session.buffer.length,
|
|
135
|
+
remoteEnded: session.ended,
|
|
136
|
+
socketClosed: session.closed,
|
|
137
|
+
error: session.error
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function serializeWebSocketSessionState(session) {
|
|
141
|
+
return {
|
|
142
|
+
bufferedBytes: session.parserBuffer.length,
|
|
143
|
+
queuedFrames: session.frames.length,
|
|
144
|
+
remoteEnded: session.ended,
|
|
145
|
+
socketClosed: session.closed,
|
|
146
|
+
closeSent: session.closeSent,
|
|
147
|
+
closeReceived: session.closeReceived,
|
|
148
|
+
error: session.error
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function normalizeWebSocketPath(path) {
|
|
152
|
+
if (!path || path.trim().length === 0) return "/";
|
|
153
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
154
|
+
}
|
|
155
|
+
function websocketOpcodeName(opcode) {
|
|
156
|
+
switch (opcode) {
|
|
157
|
+
case 1: return "text";
|
|
158
|
+
case 2: return "binary";
|
|
159
|
+
case 8: return "close";
|
|
160
|
+
case 9: return "ping";
|
|
161
|
+
case 10: return "pong";
|
|
162
|
+
default: return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function computeWebSocketAccept(requestKey) {
|
|
166
|
+
return createHash("sha1").update(`${requestKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`, "utf8").digest("base64");
|
|
167
|
+
}
|
|
168
|
+
function encodeWebSocketFrame(type, payload, closeCode, closeReason) {
|
|
169
|
+
let opcode = 1;
|
|
170
|
+
let framePayload = payload;
|
|
171
|
+
if (type === "binary") opcode = 2;
|
|
172
|
+
else if (type === "close") {
|
|
173
|
+
opcode = 8;
|
|
174
|
+
if (closeCode !== void 0 && closeCode !== null) {
|
|
175
|
+
const reasonBuffer = closeReason ? Buffer.from(closeReason, "utf8") : Buffer.alloc(0);
|
|
176
|
+
framePayload = Buffer.alloc(2 + reasonBuffer.length);
|
|
177
|
+
framePayload.writeUInt16BE(closeCode, 0);
|
|
178
|
+
reasonBuffer.copy(framePayload, 2);
|
|
179
|
+
} else if (closeReason) framePayload = Buffer.from(closeReason, "utf8");
|
|
180
|
+
} else if (type === "ping") opcode = 9;
|
|
181
|
+
else if (type === "pong") opcode = 10;
|
|
182
|
+
const maskKey = randomBytes(4);
|
|
183
|
+
const payloadLength = framePayload.length;
|
|
184
|
+
let header;
|
|
185
|
+
if (payloadLength < 126) {
|
|
186
|
+
header = Buffer.alloc(2);
|
|
187
|
+
header[1] = 128 | payloadLength;
|
|
188
|
+
} else if (payloadLength <= 65535) {
|
|
189
|
+
header = Buffer.alloc(4);
|
|
190
|
+
header[1] = 254;
|
|
191
|
+
header.writeUInt16BE(payloadLength, 2);
|
|
192
|
+
} else {
|
|
193
|
+
header = Buffer.alloc(10);
|
|
194
|
+
header[1] = 255;
|
|
195
|
+
header.writeBigUInt64BE(BigInt(payloadLength), 2);
|
|
196
|
+
}
|
|
197
|
+
header[0] = 128 | opcode;
|
|
198
|
+
const maskedPayload = Buffer.alloc(payloadLength);
|
|
199
|
+
for (let index = 0; index < payloadLength; index += 1) maskedPayload[index] = framePayload[index] ^ maskKey[index % 4];
|
|
200
|
+
return Buffer.concat([
|
|
201
|
+
header,
|
|
202
|
+
maskKey,
|
|
203
|
+
maskedPayload
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
function tryConsumeWebSocketFrame(buffer) {
|
|
207
|
+
if (buffer.length < 2) return null;
|
|
208
|
+
const first = buffer[0];
|
|
209
|
+
const second = buffer[1];
|
|
210
|
+
const fin = (first & 128) !== 0;
|
|
211
|
+
const opcode = first & 15;
|
|
212
|
+
const masked = (second & 128) !== 0;
|
|
213
|
+
let payloadLength = second & 127;
|
|
214
|
+
let cursor = 2;
|
|
215
|
+
if (payloadLength === 126) {
|
|
216
|
+
if (buffer.length < cursor + 2) return null;
|
|
217
|
+
payloadLength = buffer.readUInt16BE(cursor);
|
|
218
|
+
cursor += 2;
|
|
219
|
+
} else if (payloadLength === 127) {
|
|
220
|
+
if (buffer.length < cursor + 8) return null;
|
|
221
|
+
const bigLength = buffer.readBigUInt64BE(cursor);
|
|
222
|
+
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("WebSocket frame payload length exceeds supported limits");
|
|
223
|
+
payloadLength = Number(bigLength);
|
|
224
|
+
cursor += 8;
|
|
225
|
+
}
|
|
226
|
+
const maskKey = masked ? buffer.subarray(cursor, cursor + 4) : null;
|
|
227
|
+
if (masked) {
|
|
228
|
+
if (buffer.length < cursor + 4) return null;
|
|
229
|
+
cursor += 4;
|
|
230
|
+
}
|
|
231
|
+
if (buffer.length < cursor + payloadLength) return null;
|
|
232
|
+
const payload = buffer.subarray(cursor, cursor + payloadLength);
|
|
233
|
+
const data = Buffer.alloc(payload.length);
|
|
234
|
+
if (masked && maskKey) for (let index = 0; index < payload.length; index += 1) data[index] = payload[index] ^ maskKey[index % 4];
|
|
235
|
+
else payload.copy(data);
|
|
236
|
+
const type = websocketOpcodeName(opcode);
|
|
237
|
+
if (!type) throw new Error(`Unsupported WebSocket opcode 0x${opcode.toString(16)}`);
|
|
238
|
+
let closeCode = null;
|
|
239
|
+
let closeReason = null;
|
|
240
|
+
if (type === "close" && data.length >= 2) {
|
|
241
|
+
closeCode = data.readUInt16BE(0);
|
|
242
|
+
closeReason = data.subarray(2).toString("utf8");
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
frame: {
|
|
246
|
+
type,
|
|
247
|
+
fin,
|
|
248
|
+
opcode,
|
|
249
|
+
masked,
|
|
250
|
+
data,
|
|
251
|
+
closeCode,
|
|
252
|
+
closeReason,
|
|
253
|
+
receivedAt: Date.now()
|
|
254
|
+
},
|
|
255
|
+
bytesConsumed: cursor + payloadLength
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function wakeSessionWaiters(session) {
|
|
259
|
+
for (const waiter of session.waiters) waiter();
|
|
260
|
+
session.waiters.clear();
|
|
261
|
+
}
|
|
262
|
+
function attachBufferedSession(session) {
|
|
263
|
+
session.socket.on("data", (chunk) => {
|
|
264
|
+
session.buffer = Buffer.concat([session.buffer, chunk]);
|
|
265
|
+
wakeSessionWaiters(session);
|
|
266
|
+
});
|
|
267
|
+
session.socket.on("end", () => {
|
|
268
|
+
session.ended = true;
|
|
269
|
+
wakeSessionWaiters(session);
|
|
270
|
+
});
|
|
271
|
+
session.socket.on("close", () => {
|
|
272
|
+
session.closed = true;
|
|
273
|
+
wakeSessionWaiters(session);
|
|
274
|
+
});
|
|
275
|
+
session.socket.on("error", (error) => {
|
|
276
|
+
session.error = error.message;
|
|
277
|
+
wakeSessionWaiters(session);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function waitForSessionActivity(session, timeoutMs) {
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const onActivity = () => {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
session.waiters.delete(onActivity);
|
|
285
|
+
resolve(true);
|
|
286
|
+
};
|
|
287
|
+
const timer = setTimeout(() => {
|
|
288
|
+
session.waiters.delete(onActivity);
|
|
289
|
+
resolve(false);
|
|
290
|
+
}, timeoutMs);
|
|
291
|
+
session.waiters.add(onActivity);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function wakeWebSocketWaiters(session) {
|
|
295
|
+
for (const waiter of session.waiters) waiter();
|
|
296
|
+
session.waiters.clear();
|
|
297
|
+
}
|
|
298
|
+
function waitForWebSocketActivity(session, timeoutMs) {
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
const onActivity = () => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
session.waiters.delete(onActivity);
|
|
303
|
+
resolve(true);
|
|
304
|
+
};
|
|
305
|
+
const timer = setTimeout(() => {
|
|
306
|
+
session.waiters.delete(onActivity);
|
|
307
|
+
resolve(false);
|
|
308
|
+
}, timeoutMs);
|
|
309
|
+
session.waiters.add(onActivity);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function consumeSessionBuffer(session, delimiter, includeDelimiter, maxBytes) {
|
|
313
|
+
if (delimiter) {
|
|
314
|
+
const matchIndex = session.buffer.indexOf(delimiter);
|
|
315
|
+
if (matchIndex >= 0) {
|
|
316
|
+
const consumedBytes = matchIndex + delimiter.length;
|
|
317
|
+
const data = includeDelimiter ? session.buffer.subarray(0, consumedBytes) : session.buffer.subarray(0, matchIndex);
|
|
318
|
+
session.buffer = session.buffer.subarray(consumedBytes);
|
|
319
|
+
return {
|
|
320
|
+
data,
|
|
321
|
+
matchedDelimiter: true,
|
|
322
|
+
stopReason: "delimiter",
|
|
323
|
+
delimiterHex: delimiter.toString("hex").toUpperCase()
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (typeof maxBytes === "number" && session.buffer.length >= maxBytes) {
|
|
328
|
+
const data = session.buffer.subarray(0, maxBytes);
|
|
329
|
+
session.buffer = session.buffer.subarray(maxBytes);
|
|
330
|
+
return {
|
|
331
|
+
data,
|
|
332
|
+
matchedDelimiter: false,
|
|
333
|
+
stopReason: "maxBytes",
|
|
334
|
+
delimiterHex: delimiter ? delimiter.toString("hex").toUpperCase() : null
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if ((session.error || session.ended || session.closed) && session.buffer.length > 0) {
|
|
338
|
+
const data = session.buffer;
|
|
339
|
+
session.buffer = Buffer.alloc(0);
|
|
340
|
+
return {
|
|
341
|
+
data,
|
|
342
|
+
matchedDelimiter: false,
|
|
343
|
+
stopReason: session.error ? "error" : "closed",
|
|
344
|
+
delimiterHex: delimiter ? delimiter.toString("hex").toUpperCase() : null
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
function normalizeHex(value) {
|
|
350
|
+
return value.replace(/\s+/g, "").toUpperCase();
|
|
351
|
+
}
|
|
352
|
+
function isHex(value) {
|
|
353
|
+
return value.length > 0 && value.length % 2 === 0 && /^[0-9A-F]+$/i.test(value);
|
|
354
|
+
}
|
|
355
|
+
function tlsVersionName(major, minor) {
|
|
356
|
+
if (major === 3 && minor === 1) return "TLS 1.0";
|
|
357
|
+
if (major === 3 && minor === 2) return "TLS 1.1";
|
|
358
|
+
if (major === 3 && minor === 3) return "TLS 1.2";
|
|
359
|
+
if (major === 3 && minor === 4) return "TLS 1.3";
|
|
360
|
+
return `0x${major.toString(16).padStart(2, "0")}${minor.toString(16).padStart(2, "0")}`;
|
|
361
|
+
}
|
|
362
|
+
function contentTypeName(contentType) {
|
|
363
|
+
if (contentType === 20) return "change_cipher_spec";
|
|
364
|
+
if (contentType === 21) return "alert";
|
|
365
|
+
if (contentType === 22) return "handshake";
|
|
366
|
+
if (contentType === 23) return "application_data";
|
|
367
|
+
if (contentType === 24) return "heartbeat";
|
|
368
|
+
return "unknown";
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Parse TLS ClientHello to extract SNI, cipher suites, and extensions.
|
|
372
|
+
* Handles two input layouts:
|
|
373
|
+
* - With handshake header: [type(1) + length(3) + version(2) + random(32) + ...]
|
|
374
|
+
* - Without handshake header: [version(2) + random(32) + ...]
|
|
375
|
+
*/
|
|
376
|
+
function parseClientHello(payload) {
|
|
377
|
+
const result = {
|
|
378
|
+
cipherSuites: [],
|
|
379
|
+
extensions: []
|
|
380
|
+
};
|
|
381
|
+
const bodyOffset = payload[0] !== void 0 && payload[0] < 25 ? 4 : 0;
|
|
382
|
+
if (payload.length < bodyOffset + 38) return result;
|
|
383
|
+
const sessionIdOffset = bodyOffset + 34;
|
|
384
|
+
const sessionIdLength = payload[sessionIdOffset] ?? 0;
|
|
385
|
+
let cursor = sessionIdOffset + 1 + sessionIdLength;
|
|
386
|
+
if (cursor + 2 > payload.length) return result;
|
|
387
|
+
const cipherSuitesLength = payload.readUInt16BE(cursor);
|
|
388
|
+
cursor += 2;
|
|
389
|
+
const cipherSuitesEnd = cursor + cipherSuitesLength;
|
|
390
|
+
while (cursor + 2 <= cipherSuitesEnd) {
|
|
391
|
+
const suiteId = payload.readUInt16BE(cursor);
|
|
392
|
+
result.cipherSuites.push(CIPHER_SUITES_BY_ID[suiteId] ?? `0x${suiteId.toString(16).padStart(4, "0")}`);
|
|
393
|
+
cursor += 2;
|
|
394
|
+
}
|
|
395
|
+
cursor = cipherSuitesEnd + 1;
|
|
396
|
+
if (cursor < payload.length) {
|
|
397
|
+
const compLength = payload[cursor];
|
|
398
|
+
if (compLength !== void 0) cursor += 1 + compLength;
|
|
399
|
+
}
|
|
400
|
+
if (cursor + 2 <= payload.length) {
|
|
401
|
+
const extensionsLength = payload.readUInt16BE(cursor);
|
|
402
|
+
cursor += 2;
|
|
403
|
+
const extensionsEnd = cursor + extensionsLength;
|
|
404
|
+
while (cursor + 4 <= extensionsEnd) {
|
|
405
|
+
const extType = payload.readUInt16BE(cursor);
|
|
406
|
+
const extLength = payload.readUInt16BE(cursor + 2);
|
|
407
|
+
cursor += 4;
|
|
408
|
+
const extName = EXTENSION_NAMES[extType] ?? `unknown(0x${extType.toString(16)})`;
|
|
409
|
+
result.extensions.push({
|
|
410
|
+
type: extType,
|
|
411
|
+
name: extName,
|
|
412
|
+
length: extLength
|
|
413
|
+
});
|
|
414
|
+
if (extType === 0 && cursor + 2 <= extensionsEnd) {
|
|
415
|
+
const sniCursor = cursor + 2;
|
|
416
|
+
if (sniCursor + 3 <= extensionsEnd) {
|
|
417
|
+
if (payload[sniCursor] === 0) {
|
|
418
|
+
const sniLength = payload.readUInt16BE(sniCursor + 1);
|
|
419
|
+
const sniStart = sniCursor + 3;
|
|
420
|
+
if (sniStart + sniLength <= extensionsEnd) result.serverName = payload.subarray(sniStart, sniStart + sniLength).toString("utf8");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
cursor += extLength;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
const CIPHER_SUITES_BY_ID = {
|
|
430
|
+
156: "TLS_RSA_WITH_AES_128_GCM_SHA256",
|
|
431
|
+
157: "TLS_RSA_WITH_AES_256_GCM_SHA384",
|
|
432
|
+
52392: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
|
433
|
+
52393: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
|
434
|
+
4865: "TLS_AES_128_GCM_SHA256",
|
|
435
|
+
4866: "TLS_AES_256_GCM_SHA384",
|
|
436
|
+
4867: "TLS_CHACHA20_POLY1305_SHA256",
|
|
437
|
+
4868: "TLS_AES_128_CCM_SHA256",
|
|
438
|
+
4869: "TLS_AES_128_CCM_8_SHA256",
|
|
439
|
+
49195: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
440
|
+
49196: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
|
441
|
+
49199: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
|
442
|
+
49200: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
|
|
443
|
+
};
|
|
444
|
+
const EXTENSION_NAMES = {
|
|
445
|
+
0: "server_name",
|
|
446
|
+
1: "max_fragment_length",
|
|
447
|
+
5: "status_request",
|
|
448
|
+
10: "supported_groups",
|
|
449
|
+
13: "signature_algorithms",
|
|
450
|
+
16: "application_layer_protocol_negotiation",
|
|
451
|
+
18: "signed_certificate_timestamp",
|
|
452
|
+
23: "record_size_limit",
|
|
453
|
+
27: "compress_certificate",
|
|
454
|
+
35: "session_ticket",
|
|
455
|
+
43: "supported_versions",
|
|
456
|
+
44: "cookie",
|
|
457
|
+
45: "psk_key_exchange_modes",
|
|
458
|
+
49: "post_handshake_auth",
|
|
459
|
+
51: "key_share"
|
|
460
|
+
};
|
|
461
|
+
/**
|
|
462
|
+
* Parse a DER-encoded certificate to extract basic info.
|
|
463
|
+
*/
|
|
464
|
+
function parseDerCertificate(der) {
|
|
465
|
+
const sha256 = createHash("sha256").update(der).digest("hex").toUpperCase();
|
|
466
|
+
try {
|
|
467
|
+
const cert = new X509Certificate(der);
|
|
468
|
+
return {
|
|
469
|
+
subject: cert.subject || void 0,
|
|
470
|
+
issuer: cert.issuer || void 0,
|
|
471
|
+
serialNumber: cert.serialNumber || void 0,
|
|
472
|
+
validFrom: cert.validFrom || void 0,
|
|
473
|
+
validTo: cert.validTo || void 0,
|
|
474
|
+
sha256,
|
|
475
|
+
length: der.length
|
|
476
|
+
};
|
|
477
|
+
} catch {
|
|
478
|
+
return {
|
|
479
|
+
sha256,
|
|
480
|
+
length: der.length
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Parse a chain of DER certificates from hex.
|
|
486
|
+
*/
|
|
487
|
+
function parseCertificateChain(hexPayload) {
|
|
488
|
+
const buffer = Buffer.from(normalizeHex(hexPayload), "hex");
|
|
489
|
+
const certs = [];
|
|
490
|
+
let cursor = 0;
|
|
491
|
+
while (cursor < buffer.length - 4) if (buffer[cursor] === 48) {
|
|
492
|
+
const info = parseDerCertificate(buffer.subarray(cursor));
|
|
493
|
+
certs.push({
|
|
494
|
+
sha256: info.sha256,
|
|
495
|
+
length: info.length
|
|
496
|
+
});
|
|
497
|
+
cursor += info.length;
|
|
498
|
+
} else cursor += 1;
|
|
499
|
+
if (certs.length === 0 && buffer.length > 0) certs.push({
|
|
500
|
+
sha256: createHash("sha256").update(buffer).digest("hex").toUpperCase(),
|
|
501
|
+
length: buffer.length
|
|
502
|
+
});
|
|
503
|
+
return certs;
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/server/domains/boringssl-inspector/handlers/handler-class.ts
|
|
507
|
+
/**
|
|
508
|
+
* BoringsslInspectorHandlers — handler methods using shared utilities from ./shared.ts.
|
|
509
|
+
*/
|
|
510
|
+
var BoringsslInspectorHandlers = class {
|
|
511
|
+
extensionInvoke;
|
|
512
|
+
eventBus;
|
|
513
|
+
tcpSessions = /* @__PURE__ */ new Map();
|
|
514
|
+
tlsSessions = /* @__PURE__ */ new Map();
|
|
515
|
+
websocketSessions = /* @__PURE__ */ new Map();
|
|
516
|
+
constructor(keyLogExtractor = new TLSKeyLogExtractor()) {
|
|
517
|
+
this.keyLogExtractor = keyLogExtractor;
|
|
518
|
+
}
|
|
519
|
+
setExtensionInvoke(invoke) {
|
|
520
|
+
this.extensionInvoke = invoke;
|
|
521
|
+
}
|
|
522
|
+
setEventBus(eventBus) {
|
|
523
|
+
this.eventBus = eventBus;
|
|
524
|
+
}
|
|
525
|
+
getTcpSession(sessionId) {
|
|
526
|
+
return this.tcpSessions.get(sessionId) ?? null;
|
|
527
|
+
}
|
|
528
|
+
getTlsSession(sessionId) {
|
|
529
|
+
return this.tlsSessions.get(sessionId) ?? null;
|
|
530
|
+
}
|
|
531
|
+
getWebSocketSession(sessionId) {
|
|
532
|
+
return this.websocketSessions.get(sessionId) ?? null;
|
|
533
|
+
}
|
|
534
|
+
emitWebSocketEvent(event, payload) {
|
|
535
|
+
this.eventBus?.emit(event, payload);
|
|
536
|
+
}
|
|
537
|
+
parseWritePayload(args) {
|
|
538
|
+
const dataHex = argString(args, "dataHex");
|
|
539
|
+
const dataText = argString(args, "dataText");
|
|
540
|
+
if (!dataHex && !dataText) return {
|
|
541
|
+
ok: false,
|
|
542
|
+
error: "dataHex or dataText is required"
|
|
543
|
+
};
|
|
544
|
+
if (dataHex && dataText) return {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: "dataHex and dataText are mutually exclusive"
|
|
547
|
+
};
|
|
548
|
+
if (dataHex) {
|
|
549
|
+
const normalized = normalizeHex(dataHex);
|
|
550
|
+
if (!isHex(normalized)) return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: "dataHex must be valid even-length hexadecimal data"
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
ok: true,
|
|
556
|
+
data: Buffer.from(normalized, "hex"),
|
|
557
|
+
inputEncoding: "hex"
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
ok: true,
|
|
562
|
+
data: Buffer.from(dataText ?? "", "utf8"),
|
|
563
|
+
inputEncoding: "utf8"
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
async writeBufferedSession(session, args) {
|
|
567
|
+
if (session.socket.destroyed || session.closed) return {
|
|
568
|
+
ok: false,
|
|
569
|
+
error: `Session "${session.id}" is already closed`,
|
|
570
|
+
sessionId: session.id,
|
|
571
|
+
kind: session.kind,
|
|
572
|
+
state: serializeSessionState(session)
|
|
573
|
+
};
|
|
574
|
+
const payload = this.parseWritePayload(args);
|
|
575
|
+
if (!payload.ok) return {
|
|
576
|
+
ok: false,
|
|
577
|
+
error: payload.error,
|
|
578
|
+
sessionId: session.id,
|
|
579
|
+
kind: session.kind
|
|
580
|
+
};
|
|
581
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
582
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
583
|
+
ok: false,
|
|
584
|
+
error: "timeoutMs must be a positive number"
|
|
585
|
+
};
|
|
586
|
+
return new Promise((resolve) => {
|
|
587
|
+
let settled = false;
|
|
588
|
+
const finish = (result) => {
|
|
589
|
+
if (settled) return;
|
|
590
|
+
settled = true;
|
|
591
|
+
clearTimeout(timer);
|
|
592
|
+
session.socket.off("error", onError);
|
|
593
|
+
resolve(result);
|
|
594
|
+
};
|
|
595
|
+
const timer = setTimeout(() => {
|
|
596
|
+
finish({
|
|
597
|
+
ok: false,
|
|
598
|
+
error: "write timed out",
|
|
599
|
+
sessionId: session.id,
|
|
600
|
+
kind: session.kind,
|
|
601
|
+
state: serializeSessionState(session)
|
|
602
|
+
});
|
|
603
|
+
}, timeoutMs);
|
|
604
|
+
const onError = (error) => {
|
|
605
|
+
finish({
|
|
606
|
+
ok: false,
|
|
607
|
+
error: error.message,
|
|
608
|
+
sessionId: session.id,
|
|
609
|
+
kind: session.kind,
|
|
610
|
+
state: serializeSessionState(session)
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
session.socket.once("error", onError);
|
|
614
|
+
session.socket.write(payload.data, () => {
|
|
615
|
+
if (session.kind === "tcp") this.eventBus?.emit("tcp:session_written", {
|
|
616
|
+
sessionId: session.id,
|
|
617
|
+
byteLength: payload.data.length,
|
|
618
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
619
|
+
});
|
|
620
|
+
else this.eventBus?.emit("tls:session_written", {
|
|
621
|
+
sessionId: session.id,
|
|
622
|
+
byteLength: payload.data.length,
|
|
623
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
624
|
+
});
|
|
625
|
+
finish({
|
|
626
|
+
ok: true,
|
|
627
|
+
sessionId: session.id,
|
|
628
|
+
kind: session.kind,
|
|
629
|
+
inputEncoding: payload.inputEncoding,
|
|
630
|
+
bytesWritten: payload.data.length,
|
|
631
|
+
transport: serializeSocketAddresses(session.socket),
|
|
632
|
+
state: serializeSessionState(session)
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
async readBufferedSessionUntil(session, args) {
|
|
638
|
+
const delimiterHex = argString(args, "delimiterHex");
|
|
639
|
+
const delimiterText = argString(args, "delimiterText");
|
|
640
|
+
if (delimiterHex && delimiterText) return {
|
|
641
|
+
ok: false,
|
|
642
|
+
error: "delimiterHex and delimiterText are mutually exclusive"
|
|
643
|
+
};
|
|
644
|
+
let delimiter = null;
|
|
645
|
+
if (delimiterHex) {
|
|
646
|
+
const normalized = normalizeHex(delimiterHex);
|
|
647
|
+
if (!isHex(normalized)) return {
|
|
648
|
+
ok: false,
|
|
649
|
+
error: "delimiterHex must be valid even-length hexadecimal data"
|
|
650
|
+
};
|
|
651
|
+
delimiter = Buffer.from(normalized, "hex");
|
|
652
|
+
} else if (delimiterText !== void 0) delimiter = Buffer.from(delimiterText, "utf8");
|
|
653
|
+
if (delimiter && delimiter.length === 0) return {
|
|
654
|
+
ok: false,
|
|
655
|
+
error: "delimiter must not be empty"
|
|
656
|
+
};
|
|
657
|
+
const includeDelimiter = argBool(args, "includeDelimiter") ?? true;
|
|
658
|
+
const rawMaxBytes = argNumber(args, "maxBytes");
|
|
659
|
+
const maxBytes = rawMaxBytes === void 0 ? void 0 : Math.trunc(rawMaxBytes);
|
|
660
|
+
if (maxBytes !== void 0 && (!Number.isFinite(maxBytes) || maxBytes <= 0)) return {
|
|
661
|
+
ok: false,
|
|
662
|
+
error: "maxBytes must be a positive integer when provided"
|
|
663
|
+
};
|
|
664
|
+
if (!delimiter && maxBytes === void 0) return {
|
|
665
|
+
ok: false,
|
|
666
|
+
error: "delimiterHex, delimiterText, or maxBytes is required"
|
|
667
|
+
};
|
|
668
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
669
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
670
|
+
ok: false,
|
|
671
|
+
error: "timeoutMs must be a positive number"
|
|
672
|
+
};
|
|
673
|
+
if (session.activeRead) return {
|
|
674
|
+
ok: false,
|
|
675
|
+
error: `Session "${session.id}" already has a pending read`,
|
|
676
|
+
sessionId: session.id,
|
|
677
|
+
kind: session.kind,
|
|
678
|
+
state: serializeSessionState(session)
|
|
679
|
+
};
|
|
680
|
+
session.activeRead = true;
|
|
681
|
+
const startedAt = Date.now();
|
|
682
|
+
try {
|
|
683
|
+
while (true) {
|
|
684
|
+
const consumed = consumeSessionBuffer(session, delimiter, includeDelimiter, maxBytes);
|
|
685
|
+
if (consumed) {
|
|
686
|
+
if (session.kind === "tcp") this.eventBus?.emit("tcp:session_read", {
|
|
687
|
+
sessionId: session.id,
|
|
688
|
+
byteLength: consumed.data.length,
|
|
689
|
+
matched: consumed.matchedDelimiter,
|
|
690
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
691
|
+
});
|
|
692
|
+
else this.eventBus?.emit("tls:session_read", {
|
|
693
|
+
sessionId: session.id,
|
|
694
|
+
byteLength: consumed.data.length,
|
|
695
|
+
matched: consumed.matchedDelimiter,
|
|
696
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
ok: true,
|
|
700
|
+
sessionId: session.id,
|
|
701
|
+
kind: session.kind,
|
|
702
|
+
bytesRead: consumed.data.length,
|
|
703
|
+
matchedDelimiter: consumed.matchedDelimiter,
|
|
704
|
+
stopReason: consumed.stopReason,
|
|
705
|
+
delimiterHex: consumed.delimiterHex,
|
|
706
|
+
dataHex: consumed.data.toString("hex").toUpperCase(),
|
|
707
|
+
dataText: consumed.data.toString("utf8"),
|
|
708
|
+
elapsedMs: Date.now() - startedAt,
|
|
709
|
+
state: serializeSessionState(session)
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
if (session.error) return {
|
|
713
|
+
ok: false,
|
|
714
|
+
error: session.error,
|
|
715
|
+
sessionId: session.id,
|
|
716
|
+
kind: session.kind,
|
|
717
|
+
state: serializeSessionState(session)
|
|
718
|
+
};
|
|
719
|
+
if (session.ended || session.closed) return {
|
|
720
|
+
ok: false,
|
|
721
|
+
error: "socket closed before the requested read condition was satisfied",
|
|
722
|
+
sessionId: session.id,
|
|
723
|
+
kind: session.kind,
|
|
724
|
+
state: serializeSessionState(session)
|
|
725
|
+
};
|
|
726
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
727
|
+
if (remainingMs <= 0) return {
|
|
728
|
+
ok: false,
|
|
729
|
+
error: "read timed out",
|
|
730
|
+
sessionId: session.id,
|
|
731
|
+
kind: session.kind,
|
|
732
|
+
state: serializeSessionState(session)
|
|
733
|
+
};
|
|
734
|
+
if (!await waitForSessionActivity(session, remainingMs)) return {
|
|
735
|
+
ok: false,
|
|
736
|
+
error: "read timed out",
|
|
737
|
+
sessionId: session.id,
|
|
738
|
+
kind: session.kind,
|
|
739
|
+
state: serializeSessionState(session)
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
} finally {
|
|
743
|
+
session.activeRead = false;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
attachWebSocketSession(session) {
|
|
747
|
+
const parseBufferedFrames = () => {
|
|
748
|
+
while (session.parserBuffer.length > 0) {
|
|
749
|
+
let consumed;
|
|
750
|
+
try {
|
|
751
|
+
consumed = tryConsumeWebSocketFrame(session.parserBuffer);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
session.error = errorMessage(error);
|
|
754
|
+
session.socket.destroy();
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
if (!consumed) break;
|
|
758
|
+
session.parserBuffer = session.parserBuffer.subarray(consumed.bytesConsumed);
|
|
759
|
+
const frame = consumed.frame;
|
|
760
|
+
session.frames.push(frame);
|
|
761
|
+
if (frame.type === "ping" && !session.closeSent && !session.socket.destroyed) {
|
|
762
|
+
const pongFrame = encodeWebSocketFrame("pong", frame.data);
|
|
763
|
+
session.socket.write(pongFrame);
|
|
764
|
+
this.emitWebSocketEvent("websocket:session_written", {
|
|
765
|
+
sessionId: session.id,
|
|
766
|
+
frameType: "pong",
|
|
767
|
+
byteLength: frame.data.length,
|
|
768
|
+
automatic: true,
|
|
769
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (frame.type === "close") {
|
|
773
|
+
session.closeReceived = true;
|
|
774
|
+
if (!session.closeSent && !session.socket.destroyed) {
|
|
775
|
+
session.closeSent = true;
|
|
776
|
+
session.socket.write(encodeWebSocketFrame("close", frame.data, frame.closeCode, frame.closeReason));
|
|
777
|
+
this.emitWebSocketEvent("websocket:session_written", {
|
|
778
|
+
sessionId: session.id,
|
|
779
|
+
frameType: "close",
|
|
780
|
+
byteLength: frame.data.length,
|
|
781
|
+
automatic: true,
|
|
782
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
wakeWebSocketWaiters(session);
|
|
788
|
+
};
|
|
789
|
+
session.socket.on("data", (chunk) => {
|
|
790
|
+
session.parserBuffer = Buffer.concat([session.parserBuffer, chunk]);
|
|
791
|
+
parseBufferedFrames();
|
|
792
|
+
});
|
|
793
|
+
session.socket.on("end", () => {
|
|
794
|
+
session.ended = true;
|
|
795
|
+
wakeWebSocketWaiters(session);
|
|
796
|
+
});
|
|
797
|
+
session.socket.on("close", () => {
|
|
798
|
+
session.closed = true;
|
|
799
|
+
wakeWebSocketWaiters(session);
|
|
800
|
+
});
|
|
801
|
+
session.socket.on("error", (error) => {
|
|
802
|
+
session.error = error.message;
|
|
803
|
+
wakeWebSocketWaiters(session);
|
|
804
|
+
});
|
|
805
|
+
parseBufferedFrames();
|
|
806
|
+
}
|
|
807
|
+
async readWebSocketFrame(session, args) {
|
|
808
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
809
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
810
|
+
ok: false,
|
|
811
|
+
error: "timeoutMs must be a positive number"
|
|
812
|
+
};
|
|
813
|
+
if (session.activeRead) return {
|
|
814
|
+
ok: false,
|
|
815
|
+
error: `Session "${session.id}" already has a pending read`,
|
|
816
|
+
sessionId: session.id,
|
|
817
|
+
kind: session.kind,
|
|
818
|
+
state: serializeWebSocketSessionState(session)
|
|
819
|
+
};
|
|
820
|
+
session.activeRead = true;
|
|
821
|
+
const startedAt = Date.now();
|
|
822
|
+
try {
|
|
823
|
+
while (true) {
|
|
824
|
+
const frame = session.frames.shift();
|
|
825
|
+
if (frame) {
|
|
826
|
+
this.emitWebSocketEvent("websocket:frame_read", {
|
|
827
|
+
sessionId: session.id,
|
|
828
|
+
frameType: frame.type,
|
|
829
|
+
byteLength: frame.data.length,
|
|
830
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
831
|
+
});
|
|
832
|
+
return {
|
|
833
|
+
ok: true,
|
|
834
|
+
sessionId: session.id,
|
|
835
|
+
kind: session.kind,
|
|
836
|
+
scheme: session.scheme,
|
|
837
|
+
frameType: frame.type,
|
|
838
|
+
fin: frame.fin,
|
|
839
|
+
opcode: frame.opcode,
|
|
840
|
+
masked: frame.masked,
|
|
841
|
+
byteLength: frame.data.length,
|
|
842
|
+
dataHex: frame.data.toString("hex").toUpperCase(),
|
|
843
|
+
dataText: frame.type === "binary" ? null : frame.data.toString("utf8"),
|
|
844
|
+
closeCode: frame.closeCode,
|
|
845
|
+
closeReason: frame.closeReason,
|
|
846
|
+
elapsedMs: Date.now() - startedAt,
|
|
847
|
+
state: serializeWebSocketSessionState(session)
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
if (session.error) return {
|
|
851
|
+
ok: false,
|
|
852
|
+
error: session.error,
|
|
853
|
+
sessionId: session.id,
|
|
854
|
+
kind: session.kind,
|
|
855
|
+
state: serializeWebSocketSessionState(session)
|
|
856
|
+
};
|
|
857
|
+
if (session.closed || session.ended) return {
|
|
858
|
+
ok: false,
|
|
859
|
+
error: "socket closed before a WebSocket frame was available",
|
|
860
|
+
sessionId: session.id,
|
|
861
|
+
kind: session.kind,
|
|
862
|
+
state: serializeWebSocketSessionState(session)
|
|
863
|
+
};
|
|
864
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
865
|
+
if (remainingMs <= 0) return {
|
|
866
|
+
ok: false,
|
|
867
|
+
error: "read timed out",
|
|
868
|
+
sessionId: session.id,
|
|
869
|
+
kind: session.kind,
|
|
870
|
+
state: serializeWebSocketSessionState(session)
|
|
871
|
+
};
|
|
872
|
+
if (!await waitForWebSocketActivity(session, remainingMs)) return {
|
|
873
|
+
ok: false,
|
|
874
|
+
error: "read timed out",
|
|
875
|
+
sessionId: session.id,
|
|
876
|
+
kind: session.kind,
|
|
877
|
+
state: serializeWebSocketSessionState(session)
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
} finally {
|
|
881
|
+
session.activeRead = false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async sendWebSocketFrame(session, args) {
|
|
885
|
+
if (session.closed || session.socket.destroyed) return {
|
|
886
|
+
ok: false,
|
|
887
|
+
error: `Session "${session.id}" is already closed`,
|
|
888
|
+
sessionId: session.id,
|
|
889
|
+
kind: session.kind,
|
|
890
|
+
state: serializeWebSocketSessionState(session)
|
|
891
|
+
};
|
|
892
|
+
const frameType = argEnum(args, "frameType", new Set([
|
|
893
|
+
"text",
|
|
894
|
+
"binary",
|
|
895
|
+
"ping",
|
|
896
|
+
"pong",
|
|
897
|
+
"close"
|
|
898
|
+
]));
|
|
899
|
+
if (!frameType) return {
|
|
900
|
+
ok: false,
|
|
901
|
+
error: "frameType is required"
|
|
902
|
+
};
|
|
903
|
+
const dataHex = argString(args, "dataHex");
|
|
904
|
+
const dataText = argString(args, "dataText");
|
|
905
|
+
if (dataHex && dataText) return {
|
|
906
|
+
ok: false,
|
|
907
|
+
error: "dataHex and dataText are mutually exclusive"
|
|
908
|
+
};
|
|
909
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
910
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
911
|
+
ok: false,
|
|
912
|
+
error: "timeoutMs must be a positive number"
|
|
913
|
+
};
|
|
914
|
+
let payload = Buffer.alloc(0);
|
|
915
|
+
if (dataHex) {
|
|
916
|
+
const normalized = normalizeHex(dataHex);
|
|
917
|
+
if (!isHex(normalized)) return {
|
|
918
|
+
ok: false,
|
|
919
|
+
error: "dataHex must be valid even-length hexadecimal data"
|
|
920
|
+
};
|
|
921
|
+
payload = Buffer.from(normalized, "hex");
|
|
922
|
+
} else if (dataText !== void 0) payload = Buffer.from(dataText, "utf8");
|
|
923
|
+
let closeCode = null;
|
|
924
|
+
let closeReason = null;
|
|
925
|
+
if (frameType === "close") {
|
|
926
|
+
const rawCloseCode = argNumber(args, "closeCode");
|
|
927
|
+
if (rawCloseCode !== void 0) {
|
|
928
|
+
if (!Number.isInteger(rawCloseCode) || rawCloseCode < 1e3 || rawCloseCode > 4999) return {
|
|
929
|
+
ok: false,
|
|
930
|
+
error: "closeCode must be an integer between 1000 and 4999"
|
|
931
|
+
};
|
|
932
|
+
closeCode = rawCloseCode;
|
|
933
|
+
}
|
|
934
|
+
closeReason = argString(args, "closeReason") ?? null;
|
|
935
|
+
if (dataHex || dataText) return {
|
|
936
|
+
ok: false,
|
|
937
|
+
error: "close frames use closeCode/closeReason instead of dataHex/dataText"
|
|
938
|
+
};
|
|
939
|
+
session.closeSent = true;
|
|
940
|
+
}
|
|
941
|
+
if (frameType === "text" && dataHex) return {
|
|
942
|
+
ok: false,
|
|
943
|
+
error: "text frames require UTF-8 dataText instead of dataHex"
|
|
944
|
+
};
|
|
945
|
+
const frameBuffer = encodeWebSocketFrame(frameType, payload, closeCode, closeReason);
|
|
946
|
+
return new Promise((resolve) => {
|
|
947
|
+
let settled = false;
|
|
948
|
+
const finish = (result) => {
|
|
949
|
+
if (settled) return;
|
|
950
|
+
settled = true;
|
|
951
|
+
clearTimeout(timer);
|
|
952
|
+
session.socket.off("error", onError);
|
|
953
|
+
resolve(result);
|
|
954
|
+
};
|
|
955
|
+
const timer = setTimeout(() => {
|
|
956
|
+
finish({
|
|
957
|
+
ok: false,
|
|
958
|
+
error: "write timed out",
|
|
959
|
+
sessionId: session.id,
|
|
960
|
+
kind: session.kind,
|
|
961
|
+
state: serializeWebSocketSessionState(session)
|
|
962
|
+
});
|
|
963
|
+
}, timeoutMs);
|
|
964
|
+
const onError = (error) => {
|
|
965
|
+
finish({
|
|
966
|
+
ok: false,
|
|
967
|
+
error: error.message,
|
|
968
|
+
sessionId: session.id,
|
|
969
|
+
kind: session.kind,
|
|
970
|
+
state: serializeWebSocketSessionState(session)
|
|
971
|
+
});
|
|
972
|
+
};
|
|
973
|
+
session.socket.once("error", onError);
|
|
974
|
+
session.socket.write(frameBuffer, () => {
|
|
975
|
+
this.emitWebSocketEvent("websocket:session_written", {
|
|
976
|
+
sessionId: session.id,
|
|
977
|
+
frameType,
|
|
978
|
+
byteLength: frameType === "close" ? closeReason ? Buffer.byteLength(closeReason) + 2 : closeCode ? 2 : 0 : payload.length,
|
|
979
|
+
automatic: false,
|
|
980
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
981
|
+
});
|
|
982
|
+
finish({
|
|
983
|
+
ok: true,
|
|
984
|
+
sessionId: session.id,
|
|
985
|
+
kind: session.kind,
|
|
986
|
+
scheme: session.scheme,
|
|
987
|
+
frameType,
|
|
988
|
+
bytesWritten: frameBuffer.length,
|
|
989
|
+
payloadBytes: frameType === "close" ? closeReason ? Buffer.byteLength(closeReason) + 2 : closeCode ? 2 : 0 : payload.length,
|
|
990
|
+
state: serializeWebSocketSessionState(session)
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
async closeWebSocketSession(sessionId, args) {
|
|
996
|
+
const session = this.websocketSessions.get(sessionId);
|
|
997
|
+
if (!session) return {
|
|
998
|
+
ok: false,
|
|
999
|
+
error: `Unknown websocket sessionId "${sessionId}"`
|
|
1000
|
+
};
|
|
1001
|
+
const force = argBool(args, "force") ?? false;
|
|
1002
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 1e3;
|
|
1003
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
1004
|
+
ok: false,
|
|
1005
|
+
error: "timeoutMs must be a positive number"
|
|
1006
|
+
};
|
|
1007
|
+
const queuedFramesDiscarded = session.frames.length;
|
|
1008
|
+
if (session.closed || session.socket.destroyed) {
|
|
1009
|
+
this.websocketSessions.delete(sessionId);
|
|
1010
|
+
return {
|
|
1011
|
+
ok: true,
|
|
1012
|
+
sessionId,
|
|
1013
|
+
kind: session.kind,
|
|
1014
|
+
force,
|
|
1015
|
+
closed: true,
|
|
1016
|
+
queuedFramesDiscarded,
|
|
1017
|
+
state: serializeWebSocketSessionState(session)
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
let closeCode = null;
|
|
1021
|
+
const rawCloseCode = argNumber(args, "closeCode");
|
|
1022
|
+
if (rawCloseCode !== void 0) {
|
|
1023
|
+
if (!Number.isInteger(rawCloseCode) || rawCloseCode < 1e3 || rawCloseCode > 4999) return {
|
|
1024
|
+
ok: false,
|
|
1025
|
+
error: "closeCode must be an integer between 1000 and 4999"
|
|
1026
|
+
};
|
|
1027
|
+
closeCode = rawCloseCode;
|
|
1028
|
+
}
|
|
1029
|
+
const closeReason = argString(args, "closeReason") ?? null;
|
|
1030
|
+
return new Promise((resolve) => {
|
|
1031
|
+
let settled = false;
|
|
1032
|
+
const finish = (closed) => {
|
|
1033
|
+
if (settled) return;
|
|
1034
|
+
settled = true;
|
|
1035
|
+
clearTimeout(timer);
|
|
1036
|
+
session.socket.off("close", onClose);
|
|
1037
|
+
session.socket.off("error", onError);
|
|
1038
|
+
this.websocketSessions.delete(sessionId);
|
|
1039
|
+
this.emitWebSocketEvent("websocket:session_closed", {
|
|
1040
|
+
sessionId,
|
|
1041
|
+
reason: session.error,
|
|
1042
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1043
|
+
});
|
|
1044
|
+
resolve({
|
|
1045
|
+
ok: true,
|
|
1046
|
+
sessionId,
|
|
1047
|
+
kind: session.kind,
|
|
1048
|
+
force,
|
|
1049
|
+
closed,
|
|
1050
|
+
queuedFramesDiscarded,
|
|
1051
|
+
state: serializeWebSocketSessionState(session)
|
|
1052
|
+
});
|
|
1053
|
+
};
|
|
1054
|
+
const onClose = () => finish(true);
|
|
1055
|
+
const onError = () => finish(session.socket.destroyed || session.closed);
|
|
1056
|
+
const timer = setTimeout(() => {
|
|
1057
|
+
session.socket.destroy();
|
|
1058
|
+
finish(session.socket.destroyed || session.closed);
|
|
1059
|
+
}, timeoutMs);
|
|
1060
|
+
session.socket.once("close", onClose);
|
|
1061
|
+
session.socket.once("error", onError);
|
|
1062
|
+
if (force) {
|
|
1063
|
+
session.socket.destroy();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (!session.closeSent) {
|
|
1067
|
+
session.closeSent = true;
|
|
1068
|
+
session.socket.write(encodeWebSocketFrame("close", Buffer.alloc(0), closeCode, closeReason));
|
|
1069
|
+
this.emitWebSocketEvent("websocket:session_written", {
|
|
1070
|
+
sessionId,
|
|
1071
|
+
frameType: "close",
|
|
1072
|
+
byteLength: closeReason ? Buffer.byteLength(closeReason) + 2 : closeCode ? 2 : 0,
|
|
1073
|
+
automatic: false,
|
|
1074
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
async closeBufferedSession(sessionId, sessions, kind, args) {
|
|
1080
|
+
const session = sessions.get(sessionId);
|
|
1081
|
+
if (!session) return {
|
|
1082
|
+
ok: false,
|
|
1083
|
+
error: `Unknown ${kind} sessionId "${sessionId}"`
|
|
1084
|
+
};
|
|
1085
|
+
const force = argBool(args, "force") ?? false;
|
|
1086
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 1e3;
|
|
1087
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
1088
|
+
ok: false,
|
|
1089
|
+
error: "timeoutMs must be a positive number"
|
|
1090
|
+
};
|
|
1091
|
+
const bufferedBytesDiscarded = session.buffer.length;
|
|
1092
|
+
if (session.closed || session.socket.destroyed) {
|
|
1093
|
+
sessions.delete(sessionId);
|
|
1094
|
+
return {
|
|
1095
|
+
ok: true,
|
|
1096
|
+
sessionId,
|
|
1097
|
+
kind,
|
|
1098
|
+
force,
|
|
1099
|
+
closed: true,
|
|
1100
|
+
bufferedBytesDiscarded,
|
|
1101
|
+
state: serializeSessionState(session)
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
return new Promise((resolve) => {
|
|
1105
|
+
let settled = false;
|
|
1106
|
+
const finish = (closed) => {
|
|
1107
|
+
if (settled) return;
|
|
1108
|
+
settled = true;
|
|
1109
|
+
clearTimeout(timer);
|
|
1110
|
+
session.socket.off("close", onClose);
|
|
1111
|
+
session.socket.off("error", onError);
|
|
1112
|
+
sessions.delete(sessionId);
|
|
1113
|
+
if (kind === "tcp") this.eventBus?.emit("tcp:session_closed", {
|
|
1114
|
+
sessionId,
|
|
1115
|
+
reason: session.error,
|
|
1116
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1117
|
+
});
|
|
1118
|
+
else this.eventBus?.emit("tls:session_closed", {
|
|
1119
|
+
sessionId,
|
|
1120
|
+
reason: session.error,
|
|
1121
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1122
|
+
});
|
|
1123
|
+
resolve({
|
|
1124
|
+
ok: true,
|
|
1125
|
+
sessionId,
|
|
1126
|
+
kind,
|
|
1127
|
+
force,
|
|
1128
|
+
closed,
|
|
1129
|
+
bufferedBytesDiscarded,
|
|
1130
|
+
state: serializeSessionState(session)
|
|
1131
|
+
});
|
|
1132
|
+
};
|
|
1133
|
+
const onClose = () => finish(true);
|
|
1134
|
+
const onError = () => finish(session.socket.destroyed || session.closed);
|
|
1135
|
+
const timer = setTimeout(() => {
|
|
1136
|
+
session.socket.destroy();
|
|
1137
|
+
finish(session.socket.destroyed || session.closed);
|
|
1138
|
+
}, timeoutMs);
|
|
1139
|
+
session.socket.once("close", onClose);
|
|
1140
|
+
session.socket.once("error", onError);
|
|
1141
|
+
if (force) {
|
|
1142
|
+
session.socket.destroy();
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
session.socket.end();
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async handleTlsKeylogEnable(_args) {
|
|
1149
|
+
return {
|
|
1150
|
+
enabled: true,
|
|
1151
|
+
keyLogPath: await this.keyLogExtractor.enableKeyLog(),
|
|
1152
|
+
environmentVariable: "SSLKEYLOGFILE"
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
async handleTlsKeylogDisable(args) {
|
|
1156
|
+
const path = argString(args, "path") ?? null;
|
|
1157
|
+
if (path) await this.keyLogExtractor.disableKeyLog();
|
|
1158
|
+
else disableKeyLog();
|
|
1159
|
+
return {
|
|
1160
|
+
disabled: true,
|
|
1161
|
+
previousPath: path ?? getKeyLogFilePath()
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
async handleTlsKeylogParse(args) {
|
|
1165
|
+
const path = argString(args, "path") ?? null;
|
|
1166
|
+
const entries = this.keyLogExtractor.parseKeyLog(path ?? void 0);
|
|
1167
|
+
const summary = this.keyLogExtractor.summarizeKeyLog(path ?? void 0);
|
|
1168
|
+
return {
|
|
1169
|
+
path: path ?? this.keyLogExtractor.getKeyLogFilePath(),
|
|
1170
|
+
entries,
|
|
1171
|
+
summary
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
async handleTlsDecryptPayload(args) {
|
|
1175
|
+
const encryptedHex = argString(args, "encryptedHex") ?? null;
|
|
1176
|
+
const keyHex = argString(args, "keyHex") ?? null;
|
|
1177
|
+
const nonceHex = argString(args, "nonceHex") ?? null;
|
|
1178
|
+
const algorithm = argString(args, "algorithm") ?? "aes-256-gcm";
|
|
1179
|
+
const authTagHex = argString(args, "authTagHex") ?? null;
|
|
1180
|
+
if (!encryptedHex || !keyHex || !nonceHex) return {
|
|
1181
|
+
ok: false,
|
|
1182
|
+
error: "encryptedHex, keyHex, and nonceHex are required"
|
|
1183
|
+
};
|
|
1184
|
+
const decrypted = decryptPayload(encryptedHex, keyHex, nonceHex, algorithm, authTagHex ?? void 0);
|
|
1185
|
+
return {
|
|
1186
|
+
ok: true,
|
|
1187
|
+
algorithm,
|
|
1188
|
+
decrypted,
|
|
1189
|
+
isFailed: decrypted.startsWith("DECRYPTION_FAILED:")
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
async handleTlsKeylogSummarize(args) {
|
|
1193
|
+
const content = argString(args, "content") ?? null;
|
|
1194
|
+
if (content) return summarizeKeyLog(parseKeyLog(content));
|
|
1195
|
+
this.keyLogExtractor.parseKeyLog();
|
|
1196
|
+
return this.keyLogExtractor.summarizeKeyLog();
|
|
1197
|
+
}
|
|
1198
|
+
async handleTlsKeylogLookupSecret(args) {
|
|
1199
|
+
const clientRandom = argString(args, "clientRandom") ?? null;
|
|
1200
|
+
const label = argString(args, "label") ?? void 0;
|
|
1201
|
+
if (!clientRandom) return {
|
|
1202
|
+
ok: false,
|
|
1203
|
+
error: "clientRandom is required"
|
|
1204
|
+
};
|
|
1205
|
+
const cached = this.keyLogExtractor.lookupSecret(clientRandom);
|
|
1206
|
+
if (cached) return {
|
|
1207
|
+
ok: true,
|
|
1208
|
+
clientRandom: normalizeHex(clientRandom),
|
|
1209
|
+
secret: cached
|
|
1210
|
+
};
|
|
1211
|
+
const secret = lookupSecret(this.keyLogExtractor.parseKeyLog(), clientRandom, label);
|
|
1212
|
+
return {
|
|
1213
|
+
ok: secret !== null,
|
|
1214
|
+
clientRandom: normalizeHex(clientRandom),
|
|
1215
|
+
secret: secret ?? null
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async handleTlsCertPinBypass(args) {
|
|
1219
|
+
const target = argString(args, "target") ?? null;
|
|
1220
|
+
if (target !== "android" && target !== "ios" && target !== "desktop") return { error: "target must be one of android, ios, or desktop" };
|
|
1221
|
+
return {
|
|
1222
|
+
bypassStrategy: {
|
|
1223
|
+
android: "hook-trust-manager",
|
|
1224
|
+
ios: "replace-sec-trust-evaluation",
|
|
1225
|
+
desktop: "patch-custom-verifier"
|
|
1226
|
+
}[target],
|
|
1227
|
+
affectedDomains: ["*"],
|
|
1228
|
+
instructions: {
|
|
1229
|
+
android: ["Inject a Frida script that overrides X509TrustManager checks.", "Re-run the target flow after SSLKEYLOGFILE capture is enabled."],
|
|
1230
|
+
ios: ["Hook SecTrustEvaluateWithError and return success for the target session.", "Collect TLS keys after the app resumes the failing request."],
|
|
1231
|
+
desktop: ["Patch the custom verifier callback or disable pin comparison in the client.", "Capture a fresh handshake after the patched build starts."]
|
|
1232
|
+
}[target]
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
async handleParseHandshake(args) {
|
|
1236
|
+
const rawHex = argString(args, "rawHex") ?? null;
|
|
1237
|
+
const decrypt = args.decrypt === true;
|
|
1238
|
+
if (!rawHex) return asJsonResponse({
|
|
1239
|
+
success: false,
|
|
1240
|
+
error: "rawHex is required"
|
|
1241
|
+
});
|
|
1242
|
+
const normalizedHex = normalizeHex(rawHex);
|
|
1243
|
+
if (!isHex(normalizedHex)) return asJsonResponse({
|
|
1244
|
+
success: false,
|
|
1245
|
+
error: "Invalid hex payload"
|
|
1246
|
+
});
|
|
1247
|
+
const record = Buffer.from(normalizedHex, "hex");
|
|
1248
|
+
if (record.length < 5) return asJsonResponse({
|
|
1249
|
+
success: false,
|
|
1250
|
+
error: "TLS record is too short"
|
|
1251
|
+
});
|
|
1252
|
+
const contentType = record[0] ?? 0;
|
|
1253
|
+
const versionMajor = record[1] ?? 0;
|
|
1254
|
+
const versionMinor = record[2] ?? 0;
|
|
1255
|
+
const declaredLength = record.readUInt16BE(3);
|
|
1256
|
+
const payload = record.subarray(5);
|
|
1257
|
+
const clientHello = contentType === 22 && payload.length > 0 && payload[0] === 1 ? parseClientHello(payload) : void 0;
|
|
1258
|
+
const decryptedPreviewHex = decrypt ? (() => {
|
|
1259
|
+
const decrypted = this.keyLogExtractor.decryptPayload(normalizedHex, this.keyLogExtractor.parseKeyLog());
|
|
1260
|
+
return decrypted ? decrypted.subarray(0, 16).toString("hex").toUpperCase() : null;
|
|
1261
|
+
})() : void 0;
|
|
1262
|
+
return asJsonResponse({
|
|
1263
|
+
success: true,
|
|
1264
|
+
record: {
|
|
1265
|
+
contentType,
|
|
1266
|
+
contentTypeName: contentTypeName(contentType),
|
|
1267
|
+
version: tlsVersionName(versionMajor, versionMinor),
|
|
1268
|
+
declaredLength,
|
|
1269
|
+
actualLength: payload.length
|
|
1270
|
+
},
|
|
1271
|
+
handshake: {
|
|
1272
|
+
version: tlsVersionName(versionMajor, versionMinor),
|
|
1273
|
+
contentType: contentTypeName(contentType),
|
|
1274
|
+
...clientHello ? {
|
|
1275
|
+
type: "client_hello",
|
|
1276
|
+
serverName: clientHello.serverName,
|
|
1277
|
+
cipherSuites: clientHello.cipherSuites,
|
|
1278
|
+
extensions: clientHello.extensions
|
|
1279
|
+
} : {
|
|
1280
|
+
cipherSuite: [],
|
|
1281
|
+
extensions: []
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
sni: clientHello?.serverName ? { serverName: clientHello.serverName } : void 0,
|
|
1285
|
+
...decryptedPreviewHex !== void 0 ? { decryptedPreviewHex } : {}
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
async handleKeyLogEnable(args) {
|
|
1289
|
+
const filePath = argString(args, "filePath") ?? "/tmp/sslkeylog.log";
|
|
1290
|
+
enableKeyLog(filePath);
|
|
1291
|
+
this.eventBus?.emit("tls:keylog_started", {
|
|
1292
|
+
filePath,
|
|
1293
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1294
|
+
});
|
|
1295
|
+
return asJsonResponse({
|
|
1296
|
+
success: true,
|
|
1297
|
+
filePath,
|
|
1298
|
+
currentFilePath: getKeyLogFilePath()
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
async handleCipherSuites(args) {
|
|
1302
|
+
const filter = argString(args, "filter") ?? null;
|
|
1303
|
+
const allSuites = [
|
|
1304
|
+
"TLS_AES_128_GCM_SHA256",
|
|
1305
|
+
"TLS_AES_256_GCM_SHA384",
|
|
1306
|
+
"TLS_CHACHA20_POLY1305_SHA256",
|
|
1307
|
+
"TLS_AES_128_CCM_SHA256",
|
|
1308
|
+
"TLS_AES_128_CCM_8_SHA256",
|
|
1309
|
+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
|
1310
|
+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
|
1311
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
1312
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
|
1313
|
+
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
|
1314
|
+
"TLS_RSA_WITH_AES_256_GCM_SHA384"
|
|
1315
|
+
];
|
|
1316
|
+
const filteredSuites = filter ? allSuites.filter((suite) => suite.includes(filter)) : allSuites;
|
|
1317
|
+
return asJsonResponse({
|
|
1318
|
+
success: true,
|
|
1319
|
+
filter,
|
|
1320
|
+
total: filteredSuites.length,
|
|
1321
|
+
suites: filteredSuites
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
async handleParseCertificate(args) {
|
|
1325
|
+
const rawHex = argString(args, "rawHex") ?? null;
|
|
1326
|
+
if (!rawHex) return asJsonResponse({
|
|
1327
|
+
success: false,
|
|
1328
|
+
error: "rawHex is required"
|
|
1329
|
+
});
|
|
1330
|
+
const certs = parseCertificateChain(rawHex);
|
|
1331
|
+
return asJsonResponse({
|
|
1332
|
+
success: true,
|
|
1333
|
+
certificateCount: certs.length,
|
|
1334
|
+
fingerprints: certs.map((c) => ({
|
|
1335
|
+
sha256: c.sha256,
|
|
1336
|
+
length: c.length
|
|
1337
|
+
}))
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
async handleTlsProbeEndpoint(args) {
|
|
1341
|
+
const host = argString(args, "host")?.trim() ?? null;
|
|
1342
|
+
if (!host) return {
|
|
1343
|
+
ok: false,
|
|
1344
|
+
error: "host is required"
|
|
1345
|
+
};
|
|
1346
|
+
const port = argNumber(args, "port") ?? 443;
|
|
1347
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return {
|
|
1348
|
+
ok: false,
|
|
1349
|
+
error: "port must be an integer between 1 and 65535"
|
|
1350
|
+
};
|
|
1351
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
1352
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
1353
|
+
ok: false,
|
|
1354
|
+
error: "timeoutMs must be a positive number"
|
|
1355
|
+
};
|
|
1356
|
+
const allowInvalidCertificates = argBool(args, "allowInvalidCertificates") ?? false;
|
|
1357
|
+
const skipHostnameCheck = argBool(args, "skipHostnameCheck") ?? false;
|
|
1358
|
+
const servernameArg = argString(args, "servername")?.trim() ?? null;
|
|
1359
|
+
const alpnProtocols = [...new Set(argStringArray(args, "alpnProtocols").map((v) => v.trim()))].filter((v) => v.length > 0);
|
|
1360
|
+
let minVersion;
|
|
1361
|
+
let maxVersion;
|
|
1362
|
+
try {
|
|
1363
|
+
minVersion = argEnum(args, "minVersion", TLS_VERSION_SET);
|
|
1364
|
+
maxVersion = argEnum(args, "maxVersion", TLS_VERSION_SET);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
return {
|
|
1367
|
+
ok: false,
|
|
1368
|
+
error: errorMessage(error)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const versionOrder = [
|
|
1372
|
+
"TLSv1",
|
|
1373
|
+
"TLSv1.1",
|
|
1374
|
+
"TLSv1.2",
|
|
1375
|
+
"TLSv1.3"
|
|
1376
|
+
];
|
|
1377
|
+
if (minVersion && maxVersion && versionOrder.indexOf(minVersion) > versionOrder.indexOf(maxVersion)) return {
|
|
1378
|
+
ok: false,
|
|
1379
|
+
error: "minVersion must not be greater than maxVersion"
|
|
1380
|
+
};
|
|
1381
|
+
const ssrfCheck = validateNetworkTarget(host);
|
|
1382
|
+
if (ssrfCheck) return ssrfCheck;
|
|
1383
|
+
const caBundle = await loadProbeCaBundle(args);
|
|
1384
|
+
if (!caBundle.ok) return {
|
|
1385
|
+
ok: false,
|
|
1386
|
+
error: caBundle.error
|
|
1387
|
+
};
|
|
1388
|
+
const validationTarget = servernameArg ?? host;
|
|
1389
|
+
const requestedServername = servernameArg ?? (isIP(host) === 0 ? host : void 0);
|
|
1390
|
+
const startedAt = Date.now();
|
|
1391
|
+
return new Promise((resolve) => {
|
|
1392
|
+
let settled = false;
|
|
1393
|
+
const socket = connect(applyTlsValidationPolicy({
|
|
1394
|
+
host,
|
|
1395
|
+
port,
|
|
1396
|
+
servername: requestedServername,
|
|
1397
|
+
...minVersion ? { minVersion } : {},
|
|
1398
|
+
...maxVersion ? { maxVersion } : {},
|
|
1399
|
+
...alpnProtocols.length > 0 ? { ALPNProtocols: alpnProtocols } : {},
|
|
1400
|
+
...caBundle.ca ? { ca: caBundle.ca } : {}
|
|
1401
|
+
}, allowInvalidCertificates));
|
|
1402
|
+
const finish = (payload) => {
|
|
1403
|
+
if (settled) return;
|
|
1404
|
+
settled = true;
|
|
1405
|
+
clearTimeout(timer);
|
|
1406
|
+
socket.removeAllListeners();
|
|
1407
|
+
socket.destroy();
|
|
1408
|
+
resolve(payload);
|
|
1409
|
+
};
|
|
1410
|
+
const timer = setTimeout(() => {
|
|
1411
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1412
|
+
host,
|
|
1413
|
+
port,
|
|
1414
|
+
success: false,
|
|
1415
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1416
|
+
});
|
|
1417
|
+
finish({
|
|
1418
|
+
ok: false,
|
|
1419
|
+
error: "TLS probe timed out",
|
|
1420
|
+
target: {
|
|
1421
|
+
host,
|
|
1422
|
+
port,
|
|
1423
|
+
requestedServername: requestedServername ?? null,
|
|
1424
|
+
validationTarget
|
|
1425
|
+
},
|
|
1426
|
+
policy: {
|
|
1427
|
+
allowInvalidCertificates,
|
|
1428
|
+
skipHostnameCheck,
|
|
1429
|
+
timeoutMs,
|
|
1430
|
+
minVersion: minVersion ?? null,
|
|
1431
|
+
maxVersion: maxVersion ?? null,
|
|
1432
|
+
alpnProtocols,
|
|
1433
|
+
customCa: {
|
|
1434
|
+
source: caBundle.source,
|
|
1435
|
+
path: caBundle.path,
|
|
1436
|
+
bytes: caBundle.bytes
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
}, timeoutMs);
|
|
1441
|
+
socket.once("error", (error) => {
|
|
1442
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1443
|
+
host,
|
|
1444
|
+
port,
|
|
1445
|
+
success: false,
|
|
1446
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1447
|
+
});
|
|
1448
|
+
finish({
|
|
1449
|
+
ok: false,
|
|
1450
|
+
error: error.message,
|
|
1451
|
+
errorCode: error.code ?? null,
|
|
1452
|
+
target: {
|
|
1453
|
+
host,
|
|
1454
|
+
port,
|
|
1455
|
+
requestedServername: requestedServername ?? null,
|
|
1456
|
+
validationTarget
|
|
1457
|
+
},
|
|
1458
|
+
policy: {
|
|
1459
|
+
allowInvalidCertificates,
|
|
1460
|
+
skipHostnameCheck,
|
|
1461
|
+
timeoutMs,
|
|
1462
|
+
minVersion: minVersion ?? null,
|
|
1463
|
+
maxVersion: maxVersion ?? null,
|
|
1464
|
+
alpnProtocols,
|
|
1465
|
+
customCa: {
|
|
1466
|
+
source: caBundle.source,
|
|
1467
|
+
path: caBundle.path,
|
|
1468
|
+
bytes: caBundle.bytes
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1473
|
+
socket.once("secureConnect", () => {
|
|
1474
|
+
const handshakeMs = Date.now() - startedAt;
|
|
1475
|
+
const peerCertificate = socket.getPeerCertificate(true);
|
|
1476
|
+
const hasLeafCertificate = hasPeerCertificate(peerCertificate);
|
|
1477
|
+
const certificateChain = hasLeafCertificate ? buildPeerCertificateChain(peerCertificate) : [];
|
|
1478
|
+
const leafCertificate = certificateChain[0] ?? null;
|
|
1479
|
+
const hostnameError = skipHostnameCheck || !hasLeafCertificate ? void 0 : checkServerIdentity(validationTarget, peerCertificate);
|
|
1480
|
+
const hostnameValidation = {
|
|
1481
|
+
checked: !skipHostnameCheck,
|
|
1482
|
+
target: skipHostnameCheck ? null : validationTarget,
|
|
1483
|
+
matched: skipHostnameCheck ? null : hostnameError === void 0,
|
|
1484
|
+
error: !skipHostnameCheck && !hasLeafCertificate ? "Peer certificate was not presented by the server" : hostnameError?.message ?? null
|
|
1485
|
+
};
|
|
1486
|
+
const authorizationReasons = [
|
|
1487
|
+
socket.authorized ? "Certificate chain validated against the active trust store." : `Certificate chain validation failed: ${socket.authorizationError ?? "unknown_authority"}`,
|
|
1488
|
+
skipHostnameCheck ? "Hostname validation was skipped by request." : hostnameValidation.matched ? "Hostname validation passed." : `Hostname validation failed: ${hostnameValidation.error ?? "unknown_error"}`,
|
|
1489
|
+
!socket.authorized && allowInvalidCertificates ? "Policy allowed the probe to continue despite certificate trust failure." : null
|
|
1490
|
+
].filter((reason) => Boolean(reason));
|
|
1491
|
+
const cipher = socket.getCipher();
|
|
1492
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1493
|
+
host,
|
|
1494
|
+
port,
|
|
1495
|
+
success: true,
|
|
1496
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1497
|
+
});
|
|
1498
|
+
finish({
|
|
1499
|
+
ok: true,
|
|
1500
|
+
target: {
|
|
1501
|
+
host,
|
|
1502
|
+
port,
|
|
1503
|
+
requestedServername: requestedServername ?? null,
|
|
1504
|
+
validationTarget
|
|
1505
|
+
},
|
|
1506
|
+
policy: {
|
|
1507
|
+
allowInvalidCertificates,
|
|
1508
|
+
skipHostnameCheck,
|
|
1509
|
+
timeoutMs,
|
|
1510
|
+
minVersion: minVersion ?? null,
|
|
1511
|
+
maxVersion: maxVersion ?? null,
|
|
1512
|
+
alpnProtocols,
|
|
1513
|
+
customCa: {
|
|
1514
|
+
source: caBundle.source,
|
|
1515
|
+
path: caBundle.path,
|
|
1516
|
+
bytes: caBundle.bytes
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
transport: {
|
|
1520
|
+
protocol: socket.getProtocol() ?? null,
|
|
1521
|
+
alpnProtocol: normalizeAlpnProtocol(socket.alpnProtocol),
|
|
1522
|
+
cipher: {
|
|
1523
|
+
name: cipher.name,
|
|
1524
|
+
standardName: cipher.standardName,
|
|
1525
|
+
version: cipher.version
|
|
1526
|
+
},
|
|
1527
|
+
localAddress: socket.localAddress ?? null,
|
|
1528
|
+
localPort: socket.localPort ?? null,
|
|
1529
|
+
remoteAddress: socket.remoteAddress ?? null,
|
|
1530
|
+
remotePort: socket.remotePort ?? null,
|
|
1531
|
+
servernameSent: normalizeSocketServername(socket.servername),
|
|
1532
|
+
sessionReused: socket.isSessionReused()
|
|
1533
|
+
},
|
|
1534
|
+
authorization: {
|
|
1535
|
+
socketAuthorized: socket.authorized,
|
|
1536
|
+
authorizationError: typeof socket.authorizationError === "string" ? socket.authorizationError : socket.authorizationError?.message ?? null,
|
|
1537
|
+
hostnameValidation,
|
|
1538
|
+
policyAllowed: (socket.authorized || allowInvalidCertificates) && (skipHostnameCheck || hostnameValidation.matched === true),
|
|
1539
|
+
reasons: authorizationReasons
|
|
1540
|
+
},
|
|
1541
|
+
certificates: {
|
|
1542
|
+
leaf: leafCertificate,
|
|
1543
|
+
chain: certificateChain
|
|
1544
|
+
},
|
|
1545
|
+
timing: { handshakeMs }
|
|
1546
|
+
});
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
async handleTcpOpen(args) {
|
|
1551
|
+
const host = argString(args, "host") ?? "127.0.0.1";
|
|
1552
|
+
const port = argNumber(args, "port");
|
|
1553
|
+
if (port === void 0 || !Number.isInteger(port) || port < 1 || port > 65535) return {
|
|
1554
|
+
ok: false,
|
|
1555
|
+
error: "port must be an integer between 1 and 65535"
|
|
1556
|
+
};
|
|
1557
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
1558
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
1559
|
+
ok: false,
|
|
1560
|
+
error: "timeoutMs must be a positive number"
|
|
1561
|
+
};
|
|
1562
|
+
const noDelay = argBool(args, "noDelay") ?? true;
|
|
1563
|
+
const ssrfCheck = validateNetworkTarget(host);
|
|
1564
|
+
if (ssrfCheck) return ssrfCheck;
|
|
1565
|
+
return new Promise((resolve) => {
|
|
1566
|
+
let settled = false;
|
|
1567
|
+
const socket = new Socket();
|
|
1568
|
+
const finish = (payload) => {
|
|
1569
|
+
if (settled) return;
|
|
1570
|
+
settled = true;
|
|
1571
|
+
clearTimeout(timer);
|
|
1572
|
+
socket.off("connect", onConnect);
|
|
1573
|
+
socket.off("error", onError);
|
|
1574
|
+
resolve(payload);
|
|
1575
|
+
};
|
|
1576
|
+
const timer = setTimeout(() => {
|
|
1577
|
+
socket.destroy();
|
|
1578
|
+
finish({
|
|
1579
|
+
ok: false,
|
|
1580
|
+
error: "TCP connect timed out",
|
|
1581
|
+
target: {
|
|
1582
|
+
host,
|
|
1583
|
+
port
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
}, timeoutMs);
|
|
1587
|
+
const onError = (error) => {
|
|
1588
|
+
finish({
|
|
1589
|
+
ok: false,
|
|
1590
|
+
error: error.message,
|
|
1591
|
+
target: {
|
|
1592
|
+
host,
|
|
1593
|
+
port
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
};
|
|
1597
|
+
const onConnect = () => {
|
|
1598
|
+
socket.setNoDelay(noDelay);
|
|
1599
|
+
const sessionId = makeSessionId("tcp");
|
|
1600
|
+
const session = {
|
|
1601
|
+
id: sessionId,
|
|
1602
|
+
kind: "tcp",
|
|
1603
|
+
socket,
|
|
1604
|
+
host,
|
|
1605
|
+
port,
|
|
1606
|
+
createdAt: Date.now(),
|
|
1607
|
+
buffer: Buffer.alloc(0),
|
|
1608
|
+
ended: false,
|
|
1609
|
+
closed: false,
|
|
1610
|
+
error: null,
|
|
1611
|
+
waiters: /* @__PURE__ */ new Set(),
|
|
1612
|
+
activeRead: false
|
|
1613
|
+
};
|
|
1614
|
+
attachBufferedSession(session);
|
|
1615
|
+
this.tcpSessions.set(sessionId, session);
|
|
1616
|
+
this.eventBus?.emit("tcp:session_opened", {
|
|
1617
|
+
sessionId,
|
|
1618
|
+
host,
|
|
1619
|
+
port,
|
|
1620
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1621
|
+
});
|
|
1622
|
+
finish({
|
|
1623
|
+
ok: true,
|
|
1624
|
+
sessionId,
|
|
1625
|
+
kind: "tcp",
|
|
1626
|
+
target: {
|
|
1627
|
+
host,
|
|
1628
|
+
port
|
|
1629
|
+
},
|
|
1630
|
+
createdAt: new Date(session.createdAt).toISOString(),
|
|
1631
|
+
transport: serializeSocketAddresses(socket),
|
|
1632
|
+
state: serializeSessionState(session)
|
|
1633
|
+
});
|
|
1634
|
+
};
|
|
1635
|
+
socket.once("connect", onConnect);
|
|
1636
|
+
socket.once("error", onError);
|
|
1637
|
+
socket.connect(port, host);
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
async handleTcpWrite(args) {
|
|
1641
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
1642
|
+
if (!sessionId) return {
|
|
1643
|
+
ok: false,
|
|
1644
|
+
error: "sessionId is required"
|
|
1645
|
+
};
|
|
1646
|
+
const session = this.getTcpSession(sessionId);
|
|
1647
|
+
if (!session) return {
|
|
1648
|
+
ok: false,
|
|
1649
|
+
error: `Unknown tcp sessionId "${sessionId}"`
|
|
1650
|
+
};
|
|
1651
|
+
return this.writeBufferedSession(session, args);
|
|
1652
|
+
}
|
|
1653
|
+
async handleTcpReadUntil(args) {
|
|
1654
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
1655
|
+
if (!sessionId) return {
|
|
1656
|
+
ok: false,
|
|
1657
|
+
error: "sessionId is required"
|
|
1658
|
+
};
|
|
1659
|
+
const session = this.getTcpSession(sessionId);
|
|
1660
|
+
if (!session) return {
|
|
1661
|
+
ok: false,
|
|
1662
|
+
error: `Unknown tcp sessionId "${sessionId}"`
|
|
1663
|
+
};
|
|
1664
|
+
return this.readBufferedSessionUntil(session, args);
|
|
1665
|
+
}
|
|
1666
|
+
async handleTcpClose(args) {
|
|
1667
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
1668
|
+
if (!sessionId) return {
|
|
1669
|
+
ok: false,
|
|
1670
|
+
error: "sessionId is required"
|
|
1671
|
+
};
|
|
1672
|
+
return this.closeBufferedSession(sessionId, this.tcpSessions, "tcp", args);
|
|
1673
|
+
}
|
|
1674
|
+
async handleTlsOpen(args) {
|
|
1675
|
+
const host = argString(args, "host")?.trim() ?? null;
|
|
1676
|
+
if (!host) return {
|
|
1677
|
+
ok: false,
|
|
1678
|
+
error: "host is required"
|
|
1679
|
+
};
|
|
1680
|
+
const port = argNumber(args, "port") ?? 443;
|
|
1681
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return {
|
|
1682
|
+
ok: false,
|
|
1683
|
+
error: "port must be an integer between 1 and 65535"
|
|
1684
|
+
};
|
|
1685
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
1686
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
1687
|
+
ok: false,
|
|
1688
|
+
error: "timeoutMs must be a positive number"
|
|
1689
|
+
};
|
|
1690
|
+
const allowInvalidCertificates = argBool(args, "allowInvalidCertificates") ?? false;
|
|
1691
|
+
const skipHostnameCheck = argBool(args, "skipHostnameCheck") ?? false;
|
|
1692
|
+
const servernameArg = argString(args, "servername")?.trim() ?? null;
|
|
1693
|
+
const alpnProtocols = [...new Set(argStringArray(args, "alpnProtocols").map((value) => value.trim()))].filter((value) => value.length > 0);
|
|
1694
|
+
let minVersion;
|
|
1695
|
+
let maxVersion;
|
|
1696
|
+
try {
|
|
1697
|
+
minVersion = argEnum(args, "minVersion", TLS_VERSION_SET);
|
|
1698
|
+
maxVersion = argEnum(args, "maxVersion", TLS_VERSION_SET);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
return {
|
|
1701
|
+
ok: false,
|
|
1702
|
+
error: errorMessage(error)
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
const versionOrder = [
|
|
1706
|
+
"TLSv1",
|
|
1707
|
+
"TLSv1.1",
|
|
1708
|
+
"TLSv1.2",
|
|
1709
|
+
"TLSv1.3"
|
|
1710
|
+
];
|
|
1711
|
+
if (minVersion && maxVersion && versionOrder.indexOf(minVersion) > versionOrder.indexOf(maxVersion)) return {
|
|
1712
|
+
ok: false,
|
|
1713
|
+
error: "minVersion must not be greater than maxVersion"
|
|
1714
|
+
};
|
|
1715
|
+
const ssrfCheck = validateNetworkTarget(host);
|
|
1716
|
+
if (ssrfCheck) return ssrfCheck;
|
|
1717
|
+
const caBundle = await loadProbeCaBundle(args);
|
|
1718
|
+
if (!caBundle.ok) return {
|
|
1719
|
+
ok: false,
|
|
1720
|
+
error: caBundle.error
|
|
1721
|
+
};
|
|
1722
|
+
const target = {
|
|
1723
|
+
host,
|
|
1724
|
+
port,
|
|
1725
|
+
requestedServername: servernameArg ?? (isIP(host) === 0 ? host : void 0) ?? null,
|
|
1726
|
+
validationTarget: servernameArg ?? host
|
|
1727
|
+
};
|
|
1728
|
+
const policy = {
|
|
1729
|
+
allowInvalidCertificates,
|
|
1730
|
+
skipHostnameCheck,
|
|
1731
|
+
timeoutMs,
|
|
1732
|
+
minVersion: minVersion ?? null,
|
|
1733
|
+
maxVersion: maxVersion ?? null,
|
|
1734
|
+
alpnProtocols,
|
|
1735
|
+
customCa: {
|
|
1736
|
+
source: caBundle.source,
|
|
1737
|
+
path: caBundle.path,
|
|
1738
|
+
bytes: caBundle.bytes
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
const startedAt = Date.now();
|
|
1742
|
+
return new Promise((resolve) => {
|
|
1743
|
+
let settled = false;
|
|
1744
|
+
const socket = connect(applyTlsValidationPolicy({
|
|
1745
|
+
host,
|
|
1746
|
+
port,
|
|
1747
|
+
servername: target.requestedServername ?? void 0,
|
|
1748
|
+
...minVersion ? { minVersion } : {},
|
|
1749
|
+
...maxVersion ? { maxVersion } : {},
|
|
1750
|
+
...alpnProtocols.length > 0 ? { ALPNProtocols: alpnProtocols } : {},
|
|
1751
|
+
...caBundle.ca ? { ca: caBundle.ca } : {}
|
|
1752
|
+
}, allowInvalidCertificates));
|
|
1753
|
+
const finish = (payload) => {
|
|
1754
|
+
if (settled) return;
|
|
1755
|
+
settled = true;
|
|
1756
|
+
clearTimeout(timer);
|
|
1757
|
+
socket.off("error", onError);
|
|
1758
|
+
socket.off("secureConnect", onSecureConnect);
|
|
1759
|
+
resolve(payload);
|
|
1760
|
+
};
|
|
1761
|
+
const timer = setTimeout(() => {
|
|
1762
|
+
socket.destroy();
|
|
1763
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1764
|
+
host,
|
|
1765
|
+
port,
|
|
1766
|
+
success: false,
|
|
1767
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1768
|
+
});
|
|
1769
|
+
finish({
|
|
1770
|
+
ok: false,
|
|
1771
|
+
error: "TLS open timed out",
|
|
1772
|
+
target,
|
|
1773
|
+
policy
|
|
1774
|
+
});
|
|
1775
|
+
}, timeoutMs);
|
|
1776
|
+
const onError = (error) => {
|
|
1777
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1778
|
+
host,
|
|
1779
|
+
port,
|
|
1780
|
+
success: false,
|
|
1781
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1782
|
+
});
|
|
1783
|
+
finish({
|
|
1784
|
+
ok: false,
|
|
1785
|
+
error: error.message,
|
|
1786
|
+
errorCode: error.code ?? null,
|
|
1787
|
+
target,
|
|
1788
|
+
policy
|
|
1789
|
+
});
|
|
1790
|
+
};
|
|
1791
|
+
const onSecureConnect = () => {
|
|
1792
|
+
const handshakeMs = Date.now() - startedAt;
|
|
1793
|
+
const peerCertificate = socket.getPeerCertificate(true);
|
|
1794
|
+
const hasLeafCertificate = hasPeerCertificate(peerCertificate);
|
|
1795
|
+
const certificateChain = hasLeafCertificate ? buildPeerCertificateChain(peerCertificate) : [];
|
|
1796
|
+
const leafCertificate = certificateChain[0] ?? null;
|
|
1797
|
+
const hostnameError = skipHostnameCheck || !hasLeafCertificate ? void 0 : checkServerIdentity(target.validationTarget, peerCertificate);
|
|
1798
|
+
const hostnameValidation = {
|
|
1799
|
+
checked: !skipHostnameCheck,
|
|
1800
|
+
target: skipHostnameCheck ? null : target.validationTarget,
|
|
1801
|
+
matched: skipHostnameCheck ? null : hostnameError === void 0,
|
|
1802
|
+
error: !skipHostnameCheck && !hasLeafCertificate ? "Peer certificate was not presented by the server" : hostnameError?.message ?? null
|
|
1803
|
+
};
|
|
1804
|
+
const authorizationReasons = [
|
|
1805
|
+
socket.authorized ? "Certificate chain validated against the active trust store." : `Certificate chain validation failed: ${socket.authorizationError ?? "unknown_authority"}`,
|
|
1806
|
+
skipHostnameCheck ? "Hostname validation was skipped by request." : hostnameValidation.matched ? "Hostname validation passed." : `Hostname validation failed: ${hostnameValidation.error ?? "unknown_error"}`,
|
|
1807
|
+
!socket.authorized && allowInvalidCertificates ? "Policy allowed the session to continue despite certificate trust failure." : null
|
|
1808
|
+
].filter((reason) => Boolean(reason));
|
|
1809
|
+
const cipher = socket.getCipher();
|
|
1810
|
+
const metadata = {
|
|
1811
|
+
target,
|
|
1812
|
+
policy,
|
|
1813
|
+
transport: {
|
|
1814
|
+
protocol: socket.getProtocol() ?? null,
|
|
1815
|
+
alpnProtocol: normalizeAlpnProtocol(socket.alpnProtocol),
|
|
1816
|
+
cipher: {
|
|
1817
|
+
name: cipher.name,
|
|
1818
|
+
standardName: cipher.standardName,
|
|
1819
|
+
version: cipher.version
|
|
1820
|
+
},
|
|
1821
|
+
localAddress: socket.localAddress ?? null,
|
|
1822
|
+
localPort: socket.localPort ?? null,
|
|
1823
|
+
remoteAddress: socket.remoteAddress ?? null,
|
|
1824
|
+
remotePort: socket.remotePort ?? null,
|
|
1825
|
+
servernameSent: normalizeSocketServername(socket.servername),
|
|
1826
|
+
sessionReused: socket.isSessionReused()
|
|
1827
|
+
},
|
|
1828
|
+
authorization: {
|
|
1829
|
+
socketAuthorized: socket.authorized,
|
|
1830
|
+
authorizationError: typeof socket.authorizationError === "string" ? socket.authorizationError : socket.authorizationError?.message ?? null,
|
|
1831
|
+
hostnameValidation,
|
|
1832
|
+
policyAllowed: (socket.authorized || allowInvalidCertificates) && (skipHostnameCheck || hostnameValidation.matched === true),
|
|
1833
|
+
reasons: authorizationReasons
|
|
1834
|
+
},
|
|
1835
|
+
certificates: {
|
|
1836
|
+
leaf: leafCertificate,
|
|
1837
|
+
chain: certificateChain
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
if (!metadata.authorization.policyAllowed) {
|
|
1841
|
+
socket.destroy();
|
|
1842
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1843
|
+
host,
|
|
1844
|
+
port,
|
|
1845
|
+
success: false,
|
|
1846
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1847
|
+
});
|
|
1848
|
+
finish({
|
|
1849
|
+
ok: false,
|
|
1850
|
+
error: "TLS session authorization failed",
|
|
1851
|
+
...metadata,
|
|
1852
|
+
timing: { handshakeMs }
|
|
1853
|
+
});
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const sessionId = makeSessionId("tls");
|
|
1857
|
+
const session = {
|
|
1858
|
+
id: sessionId,
|
|
1859
|
+
kind: "tls",
|
|
1860
|
+
socket,
|
|
1861
|
+
host,
|
|
1862
|
+
port,
|
|
1863
|
+
createdAt: Date.now(),
|
|
1864
|
+
buffer: Buffer.alloc(0),
|
|
1865
|
+
ended: false,
|
|
1866
|
+
closed: false,
|
|
1867
|
+
error: null,
|
|
1868
|
+
waiters: /* @__PURE__ */ new Set(),
|
|
1869
|
+
activeRead: false,
|
|
1870
|
+
metadata
|
|
1871
|
+
};
|
|
1872
|
+
attachBufferedSession(session);
|
|
1873
|
+
this.tlsSessions.set(sessionId, session);
|
|
1874
|
+
this.eventBus?.emit("tls:session_opened", {
|
|
1875
|
+
sessionId,
|
|
1876
|
+
host,
|
|
1877
|
+
port,
|
|
1878
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1879
|
+
});
|
|
1880
|
+
this.eventBus?.emit("tls:probe_completed", {
|
|
1881
|
+
host,
|
|
1882
|
+
port,
|
|
1883
|
+
success: true,
|
|
1884
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1885
|
+
});
|
|
1886
|
+
finish({
|
|
1887
|
+
ok: true,
|
|
1888
|
+
sessionId,
|
|
1889
|
+
kind: "tls",
|
|
1890
|
+
...metadata,
|
|
1891
|
+
timing: { handshakeMs },
|
|
1892
|
+
state: serializeSessionState(session)
|
|
1893
|
+
});
|
|
1894
|
+
};
|
|
1895
|
+
socket.once("error", onError);
|
|
1896
|
+
socket.once("secureConnect", onSecureConnect);
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
async handleTlsWrite(args) {
|
|
1900
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
1901
|
+
if (!sessionId) return {
|
|
1902
|
+
ok: false,
|
|
1903
|
+
error: "sessionId is required"
|
|
1904
|
+
};
|
|
1905
|
+
const session = this.getTlsSession(sessionId);
|
|
1906
|
+
if (!session) return {
|
|
1907
|
+
ok: false,
|
|
1908
|
+
error: `Unknown tls sessionId "${sessionId}"`
|
|
1909
|
+
};
|
|
1910
|
+
return this.writeBufferedSession(session, args);
|
|
1911
|
+
}
|
|
1912
|
+
async handleTlsReadUntil(args) {
|
|
1913
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
1914
|
+
if (!sessionId) return {
|
|
1915
|
+
ok: false,
|
|
1916
|
+
error: "sessionId is required"
|
|
1917
|
+
};
|
|
1918
|
+
const session = this.getTlsSession(sessionId);
|
|
1919
|
+
if (!session) return {
|
|
1920
|
+
ok: false,
|
|
1921
|
+
error: `Unknown tls sessionId "${sessionId}"`
|
|
1922
|
+
};
|
|
1923
|
+
return this.readBufferedSessionUntil(session, args);
|
|
1924
|
+
}
|
|
1925
|
+
async handleTlsClose(args) {
|
|
1926
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
1927
|
+
if (!sessionId) return {
|
|
1928
|
+
ok: false,
|
|
1929
|
+
error: "sessionId is required"
|
|
1930
|
+
};
|
|
1931
|
+
return this.closeBufferedSession(sessionId, this.tlsSessions, "tls", args);
|
|
1932
|
+
}
|
|
1933
|
+
async handleWebSocketOpen(args) {
|
|
1934
|
+
const rawUrl = argString(args, "url")?.trim() ?? null;
|
|
1935
|
+
const rawHost = argString(args, "host")?.trim() ?? null;
|
|
1936
|
+
const rawPath = argString(args, "path")?.trim() ?? null;
|
|
1937
|
+
const rawPort = argNumber(args, "port");
|
|
1938
|
+
const rawScheme = argString(args, "scheme")?.trim() ?? null;
|
|
1939
|
+
if (rawUrl && (rawHost || rawPath || rawPort !== void 0 || rawScheme)) return {
|
|
1940
|
+
ok: false,
|
|
1941
|
+
error: "url is mutually exclusive with explicit scheme/host/port/path inputs"
|
|
1942
|
+
};
|
|
1943
|
+
let scheme = "ws";
|
|
1944
|
+
let host = rawHost;
|
|
1945
|
+
let port = rawPort ?? void 0;
|
|
1946
|
+
let path = normalizeWebSocketPath(rawPath);
|
|
1947
|
+
let url;
|
|
1948
|
+
if (rawUrl) {
|
|
1949
|
+
let parsedUrl;
|
|
1950
|
+
try {
|
|
1951
|
+
parsedUrl = new URL(rawUrl);
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
return {
|
|
1954
|
+
ok: false,
|
|
1955
|
+
error: `Invalid url: ${errorMessage(error)}`
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
if (parsedUrl.protocol !== "ws:" && parsedUrl.protocol !== "wss:") return {
|
|
1959
|
+
ok: false,
|
|
1960
|
+
error: "url must use ws:// or wss:// protocol"
|
|
1961
|
+
};
|
|
1962
|
+
scheme = parsedUrl.protocol === "wss:" ? "wss" : "ws";
|
|
1963
|
+
host = parsedUrl.hostname;
|
|
1964
|
+
port = parsedUrl.port.length > 0 ? Number(parsedUrl.port) : scheme === "wss" ? 443 : 80;
|
|
1965
|
+
path = normalizeWebSocketPath(`${parsedUrl.pathname}${parsedUrl.search}`);
|
|
1966
|
+
url = `${scheme}://${parsedUrl.host}${path}`;
|
|
1967
|
+
} else {
|
|
1968
|
+
if (!host) return {
|
|
1969
|
+
ok: false,
|
|
1970
|
+
error: "host or url is required"
|
|
1971
|
+
};
|
|
1972
|
+
if (rawScheme) {
|
|
1973
|
+
if (rawScheme !== "ws" && rawScheme !== "wss") return {
|
|
1974
|
+
ok: false,
|
|
1975
|
+
error: "scheme must be ws or wss"
|
|
1976
|
+
};
|
|
1977
|
+
scheme = rawScheme;
|
|
1978
|
+
}
|
|
1979
|
+
port ??= scheme === "wss" ? 443 : 80;
|
|
1980
|
+
const authority = port === (scheme === "wss" ? 443 : 80) ? host : `${host}:${String(port)}`;
|
|
1981
|
+
url = `${scheme}://${authority}${path}`;
|
|
1982
|
+
}
|
|
1983
|
+
if (!host || !port || !Number.isInteger(port) || port < 1 || port > 65535) return {
|
|
1984
|
+
ok: false,
|
|
1985
|
+
error: "port must be an integer between 1 and 65535"
|
|
1986
|
+
};
|
|
1987
|
+
const timeoutMs = argNumber(args, "timeoutMs") ?? 5e3;
|
|
1988
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
|
|
1989
|
+
ok: false,
|
|
1990
|
+
error: "timeoutMs must be a positive number"
|
|
1991
|
+
};
|
|
1992
|
+
const subprotocols = [...new Set(argStringArray(args, "subprotocols").map((value) => value.trim()))].filter((value) => value.length > 0);
|
|
1993
|
+
const ssrfCheck = validateNetworkTarget(host);
|
|
1994
|
+
if (ssrfCheck) return ssrfCheck;
|
|
1995
|
+
const allowInvalidCertificates = argBool(args, "allowInvalidCertificates") ?? false;
|
|
1996
|
+
const skipHostnameCheck = argBool(args, "skipHostnameCheck") ?? false;
|
|
1997
|
+
const servernameArg = argString(args, "servername")?.trim() ?? null;
|
|
1998
|
+
const alpnProtocols = [...new Set(argStringArray(args, "alpnProtocols").map((value) => value.trim()))].filter((value) => value.length > 0);
|
|
1999
|
+
let minVersion;
|
|
2000
|
+
let maxVersion;
|
|
2001
|
+
try {
|
|
2002
|
+
minVersion = argEnum(args, "minVersion", TLS_VERSION_SET);
|
|
2003
|
+
maxVersion = argEnum(args, "maxVersion", TLS_VERSION_SET);
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
return {
|
|
2006
|
+
ok: false,
|
|
2007
|
+
error: errorMessage(error)
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
const versionOrder = [
|
|
2011
|
+
"TLSv1",
|
|
2012
|
+
"TLSv1.1",
|
|
2013
|
+
"TLSv1.2",
|
|
2014
|
+
"TLSv1.3"
|
|
2015
|
+
];
|
|
2016
|
+
if (minVersion && maxVersion && versionOrder.indexOf(minVersion) > versionOrder.indexOf(maxVersion)) return {
|
|
2017
|
+
ok: false,
|
|
2018
|
+
error: "minVersion must not be greater than maxVersion"
|
|
2019
|
+
};
|
|
2020
|
+
const caBundle = scheme === "wss" ? await loadProbeCaBundle(args) : {
|
|
2021
|
+
ok: true,
|
|
2022
|
+
ca: void 0,
|
|
2023
|
+
source: null,
|
|
2024
|
+
path: null,
|
|
2025
|
+
bytes: null
|
|
2026
|
+
};
|
|
2027
|
+
if (!caBundle.ok) return {
|
|
2028
|
+
ok: false,
|
|
2029
|
+
error: caBundle.error
|
|
2030
|
+
};
|
|
2031
|
+
const target = {
|
|
2032
|
+
scheme,
|
|
2033
|
+
url,
|
|
2034
|
+
host,
|
|
2035
|
+
port,
|
|
2036
|
+
path,
|
|
2037
|
+
requestedServername: scheme === "wss" ? servernameArg ?? (isIP(host) === 0 ? host : void 0) ?? null : null,
|
|
2038
|
+
validationTarget: scheme === "wss" ? servernameArg ?? host : null
|
|
2039
|
+
};
|
|
2040
|
+
const requestKey = randomBytes(16).toString("base64");
|
|
2041
|
+
const acceptKey = computeWebSocketAccept(requestKey);
|
|
2042
|
+
const startedAt = Date.now();
|
|
2043
|
+
return new Promise((resolve) => {
|
|
2044
|
+
let settled = false;
|
|
2045
|
+
let handshakeBuffer = Buffer.alloc(0);
|
|
2046
|
+
let transport = null;
|
|
2047
|
+
let authorization = null;
|
|
2048
|
+
let certificates = null;
|
|
2049
|
+
const socket = scheme === "wss" ? connect(applyTlsValidationPolicy({
|
|
2050
|
+
host,
|
|
2051
|
+
port,
|
|
2052
|
+
servername: target.requestedServername ?? void 0,
|
|
2053
|
+
...minVersion ? { minVersion } : {},
|
|
2054
|
+
...maxVersion ? { maxVersion } : {},
|
|
2055
|
+
...alpnProtocols.length > 0 ? { ALPNProtocols: alpnProtocols } : {},
|
|
2056
|
+
...caBundle.ca ? { ca: caBundle.ca } : {}
|
|
2057
|
+
}, allowInvalidCertificates)) : new Socket();
|
|
2058
|
+
const finish = (payload) => {
|
|
2059
|
+
if (settled) return;
|
|
2060
|
+
settled = true;
|
|
2061
|
+
clearTimeout(timer);
|
|
2062
|
+
socket.off("error", onError);
|
|
2063
|
+
socket.off("connect", onConnect);
|
|
2064
|
+
socket.off("secureConnect", onSecureConnect);
|
|
2065
|
+
socket.off("data", onHandshakeData);
|
|
2066
|
+
resolve(payload);
|
|
2067
|
+
};
|
|
2068
|
+
const buildHandshakeRequest = () => {
|
|
2069
|
+
const hostHeader = port === (scheme === "wss" ? 443 : 80) ? host : `${host}:${String(port)}`;
|
|
2070
|
+
const lines = [
|
|
2071
|
+
`GET ${path} HTTP/1.1`,
|
|
2072
|
+
`Host: ${hostHeader}`,
|
|
2073
|
+
"Upgrade: websocket",
|
|
2074
|
+
"Connection: Upgrade",
|
|
2075
|
+
`Sec-WebSocket-Key: ${requestKey}`,
|
|
2076
|
+
"Sec-WebSocket-Version: 13"
|
|
2077
|
+
];
|
|
2078
|
+
if (subprotocols.length > 0) lines.push(`Sec-WebSocket-Protocol: ${subprotocols.join(", ")}`);
|
|
2079
|
+
lines.push("", "");
|
|
2080
|
+
return Buffer.from(lines.join("\r\n"), "utf8");
|
|
2081
|
+
};
|
|
2082
|
+
const timer = setTimeout(() => {
|
|
2083
|
+
socket.destroy();
|
|
2084
|
+
finish({
|
|
2085
|
+
ok: false,
|
|
2086
|
+
error: "WebSocket open timed out",
|
|
2087
|
+
target
|
|
2088
|
+
});
|
|
2089
|
+
}, timeoutMs);
|
|
2090
|
+
const onError = (error) => {
|
|
2091
|
+
finish({
|
|
2092
|
+
ok: false,
|
|
2093
|
+
error: error.message,
|
|
2094
|
+
errorCode: error.code ?? null,
|
|
2095
|
+
target
|
|
2096
|
+
});
|
|
2097
|
+
};
|
|
2098
|
+
const sendHandshake = () => {
|
|
2099
|
+
socket.write(buildHandshakeRequest());
|
|
2100
|
+
};
|
|
2101
|
+
const onConnect = () => {
|
|
2102
|
+
if (socket instanceof Socket) socket.setNoDelay(true);
|
|
2103
|
+
transport = {
|
|
2104
|
+
...serializeSocketAddresses(socket),
|
|
2105
|
+
protocol: null,
|
|
2106
|
+
alpnProtocol: null,
|
|
2107
|
+
servernameSent: null,
|
|
2108
|
+
sessionReused: null
|
|
2109
|
+
};
|
|
2110
|
+
sendHandshake();
|
|
2111
|
+
};
|
|
2112
|
+
const onSecureConnect = () => {
|
|
2113
|
+
if (!(socket instanceof Object) || !("getPeerCertificate" in socket)) {
|
|
2114
|
+
finish({
|
|
2115
|
+
ok: false,
|
|
2116
|
+
error: "Expected a TLS socket for wss session",
|
|
2117
|
+
target
|
|
2118
|
+
});
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const tlsSocket = socket;
|
|
2122
|
+
const peerCertificate = tlsSocket.getPeerCertificate(true);
|
|
2123
|
+
const hasLeafCertificate = hasPeerCertificate(peerCertificate);
|
|
2124
|
+
const certificateChain = hasLeafCertificate ? buildPeerCertificateChain(peerCertificate) : [];
|
|
2125
|
+
const leafCertificate = certificateChain[0] ?? null;
|
|
2126
|
+
const hostnameError = skipHostnameCheck || !hasLeafCertificate || !target.validationTarget ? void 0 : checkServerIdentity(target.validationTarget, peerCertificate);
|
|
2127
|
+
const hostnameValidation = {
|
|
2128
|
+
checked: !skipHostnameCheck,
|
|
2129
|
+
target: skipHostnameCheck ? null : target.validationTarget,
|
|
2130
|
+
matched: skipHostnameCheck ? null : hostnameError === void 0,
|
|
2131
|
+
error: !skipHostnameCheck && !hasLeafCertificate ? "Peer certificate was not presented by the server" : hostnameError?.message ?? null
|
|
2132
|
+
};
|
|
2133
|
+
const authorizationReasons = [
|
|
2134
|
+
tlsSocket.authorized ? "Certificate chain validated against the active trust store." : `Certificate chain validation failed: ${tlsSocket.authorizationError ?? "unknown_authority"}`,
|
|
2135
|
+
skipHostnameCheck ? "Hostname validation was skipped by request." : hostnameValidation.matched ? "Hostname validation passed." : `Hostname validation failed: ${hostnameValidation.error ?? "unknown_error"}`,
|
|
2136
|
+
!tlsSocket.authorized && allowInvalidCertificates ? "Policy allowed the session to continue despite certificate trust failure." : null
|
|
2137
|
+
].filter((reason) => Boolean(reason));
|
|
2138
|
+
authorization = {
|
|
2139
|
+
socketAuthorized: tlsSocket.authorized,
|
|
2140
|
+
authorizationError: typeof tlsSocket.authorizationError === "string" ? tlsSocket.authorizationError : tlsSocket.authorizationError?.message ?? null,
|
|
2141
|
+
hostnameValidation,
|
|
2142
|
+
policyAllowed: (tlsSocket.authorized || allowInvalidCertificates) && (skipHostnameCheck || hostnameValidation.matched === true),
|
|
2143
|
+
reasons: authorizationReasons
|
|
2144
|
+
};
|
|
2145
|
+
certificates = {
|
|
2146
|
+
leaf: leafCertificate,
|
|
2147
|
+
chain: certificateChain
|
|
2148
|
+
};
|
|
2149
|
+
transport = {
|
|
2150
|
+
...serializeSocketAddresses(tlsSocket),
|
|
2151
|
+
protocol: tlsSocket.getProtocol() ?? null,
|
|
2152
|
+
alpnProtocol: normalizeAlpnProtocol(tlsSocket.alpnProtocol),
|
|
2153
|
+
servernameSent: normalizeSocketServername(tlsSocket.servername),
|
|
2154
|
+
sessionReused: tlsSocket.isSessionReused()
|
|
2155
|
+
};
|
|
2156
|
+
if (!authorization.policyAllowed) {
|
|
2157
|
+
tlsSocket.destroy();
|
|
2158
|
+
finish({
|
|
2159
|
+
ok: false,
|
|
2160
|
+
error: "WebSocket TLS authorization failed",
|
|
2161
|
+
target,
|
|
2162
|
+
authorization,
|
|
2163
|
+
certificates
|
|
2164
|
+
});
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
sendHandshake();
|
|
2168
|
+
};
|
|
2169
|
+
const onHandshakeData = (chunk) => {
|
|
2170
|
+
handshakeBuffer = Buffer.concat([handshakeBuffer, chunk]);
|
|
2171
|
+
const headerEnd = handshakeBuffer.indexOf("\r\n\r\n");
|
|
2172
|
+
if (headerEnd < 0) return;
|
|
2173
|
+
const lines = handshakeBuffer.subarray(0, headerEnd).toString("utf8").split("\r\n");
|
|
2174
|
+
const statusLine = lines.shift() ?? "";
|
|
2175
|
+
if (!/^HTTP\/1\.1 101\b/.test(statusLine)) {
|
|
2176
|
+
socket.destroy();
|
|
2177
|
+
finish({
|
|
2178
|
+
ok: false,
|
|
2179
|
+
error: `Unexpected WebSocket upgrade response: ${statusLine}`,
|
|
2180
|
+
target
|
|
2181
|
+
});
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const headers = /* @__PURE__ */ new Map();
|
|
2185
|
+
for (const line of lines) {
|
|
2186
|
+
const separator = line.indexOf(":");
|
|
2187
|
+
if (separator <= 0) continue;
|
|
2188
|
+
const name = line.slice(0, separator).trim().toLowerCase();
|
|
2189
|
+
const value = line.slice(separator + 1).trim();
|
|
2190
|
+
headers.set(name, value);
|
|
2191
|
+
}
|
|
2192
|
+
const upgrade = headers.get("upgrade")?.toLowerCase() ?? "";
|
|
2193
|
+
const connection = headers.get("connection")?.toLowerCase() ?? "";
|
|
2194
|
+
const responseAcceptKey = headers.get("sec-websocket-accept") ?? null;
|
|
2195
|
+
if (upgrade !== "websocket") {
|
|
2196
|
+
socket.destroy();
|
|
2197
|
+
finish({
|
|
2198
|
+
ok: false,
|
|
2199
|
+
error: "Upgrade header did not confirm websocket",
|
|
2200
|
+
target
|
|
2201
|
+
});
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (!connection.split(",").map((part) => part.trim()).includes("upgrade")) {
|
|
2205
|
+
socket.destroy();
|
|
2206
|
+
finish({
|
|
2207
|
+
ok: false,
|
|
2208
|
+
error: "Connection header did not confirm upgrade",
|
|
2209
|
+
target
|
|
2210
|
+
});
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (responseAcceptKey !== acceptKey) {
|
|
2214
|
+
socket.destroy();
|
|
2215
|
+
finish({
|
|
2216
|
+
ok: false,
|
|
2217
|
+
error: "sec-websocket-accept did not match the client key",
|
|
2218
|
+
target
|
|
2219
|
+
});
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const negotiatedSubprotocol = headers.get("sec-websocket-protocol") ?? null;
|
|
2223
|
+
if (negotiatedSubprotocol && !subprotocols.includes(negotiatedSubprotocol)) {
|
|
2224
|
+
socket.destroy();
|
|
2225
|
+
finish({
|
|
2226
|
+
ok: false,
|
|
2227
|
+
error: `Server selected unexpected subprotocol "${negotiatedSubprotocol}"`,
|
|
2228
|
+
target
|
|
2229
|
+
});
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const sessionId = makeSessionId("websocket");
|
|
2233
|
+
const session = {
|
|
2234
|
+
id: sessionId,
|
|
2235
|
+
kind: "websocket",
|
|
2236
|
+
scheme,
|
|
2237
|
+
socket,
|
|
2238
|
+
host,
|
|
2239
|
+
port,
|
|
2240
|
+
path,
|
|
2241
|
+
createdAt: Date.now(),
|
|
2242
|
+
parserBuffer: handshakeBuffer.subarray(headerEnd + 4),
|
|
2243
|
+
frames: [],
|
|
2244
|
+
ended: false,
|
|
2245
|
+
closed: false,
|
|
2246
|
+
error: null,
|
|
2247
|
+
waiters: /* @__PURE__ */ new Set(),
|
|
2248
|
+
activeRead: false,
|
|
2249
|
+
closeSent: false,
|
|
2250
|
+
closeReceived: false,
|
|
2251
|
+
metadata: {
|
|
2252
|
+
target,
|
|
2253
|
+
handshake: {
|
|
2254
|
+
requestKey,
|
|
2255
|
+
acceptKey,
|
|
2256
|
+
responseAcceptKey,
|
|
2257
|
+
subprotocol: negotiatedSubprotocol
|
|
2258
|
+
},
|
|
2259
|
+
transport: transport ?? {
|
|
2260
|
+
...serializeSocketAddresses(socket),
|
|
2261
|
+
protocol: null,
|
|
2262
|
+
alpnProtocol: null,
|
|
2263
|
+
servernameSent: null,
|
|
2264
|
+
sessionReused: null
|
|
2265
|
+
},
|
|
2266
|
+
authorization,
|
|
2267
|
+
certificates
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
this.attachWebSocketSession(session);
|
|
2271
|
+
this.websocketSessions.set(sessionId, session);
|
|
2272
|
+
this.emitWebSocketEvent("websocket:session_opened", {
|
|
2273
|
+
sessionId,
|
|
2274
|
+
scheme,
|
|
2275
|
+
host,
|
|
2276
|
+
port,
|
|
2277
|
+
path,
|
|
2278
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2279
|
+
});
|
|
2280
|
+
finish({
|
|
2281
|
+
ok: true,
|
|
2282
|
+
sessionId,
|
|
2283
|
+
kind: session.kind,
|
|
2284
|
+
scheme,
|
|
2285
|
+
target,
|
|
2286
|
+
handshake: session.metadata.handshake,
|
|
2287
|
+
transport: session.metadata.transport,
|
|
2288
|
+
authorization: session.metadata.authorization,
|
|
2289
|
+
certificates: session.metadata.certificates,
|
|
2290
|
+
timing: { handshakeMs: Date.now() - startedAt },
|
|
2291
|
+
state: serializeWebSocketSessionState(session)
|
|
2292
|
+
});
|
|
2293
|
+
};
|
|
2294
|
+
socket.once("error", onError);
|
|
2295
|
+
socket.on("data", onHandshakeData);
|
|
2296
|
+
if (scheme === "wss") socket.once("secureConnect", onSecureConnect);
|
|
2297
|
+
else {
|
|
2298
|
+
socket.once("connect", onConnect);
|
|
2299
|
+
socket.connect(port, host);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
async handleWebSocketSendFrame(args) {
|
|
2304
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
2305
|
+
if (!sessionId) return {
|
|
2306
|
+
ok: false,
|
|
2307
|
+
error: "sessionId is required"
|
|
2308
|
+
};
|
|
2309
|
+
const session = this.getWebSocketSession(sessionId);
|
|
2310
|
+
if (!session) return {
|
|
2311
|
+
ok: false,
|
|
2312
|
+
error: `Unknown websocket sessionId "${sessionId}"`
|
|
2313
|
+
};
|
|
2314
|
+
return this.sendWebSocketFrame(session, args);
|
|
2315
|
+
}
|
|
2316
|
+
async handleWebSocketReadFrame(args) {
|
|
2317
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
2318
|
+
if (!sessionId) return {
|
|
2319
|
+
ok: false,
|
|
2320
|
+
error: "sessionId is required"
|
|
2321
|
+
};
|
|
2322
|
+
const session = this.getWebSocketSession(sessionId);
|
|
2323
|
+
if (!session) return {
|
|
2324
|
+
ok: false,
|
|
2325
|
+
error: `Unknown websocket sessionId "${sessionId}"`
|
|
2326
|
+
};
|
|
2327
|
+
return this.readWebSocketFrame(session, args);
|
|
2328
|
+
}
|
|
2329
|
+
async handleWebSocketClose(args) {
|
|
2330
|
+
const sessionId = argString(args, "sessionId")?.trim() ?? null;
|
|
2331
|
+
if (!sessionId) return {
|
|
2332
|
+
ok: false,
|
|
2333
|
+
error: "sessionId is required"
|
|
2334
|
+
};
|
|
2335
|
+
return this.closeWebSocketSession(sessionId, args);
|
|
2336
|
+
}
|
|
2337
|
+
async handleBypassCertPinning(args) {
|
|
2338
|
+
if (this.extensionInvoke) try {
|
|
2339
|
+
const result = await this.extensionInvoke(args);
|
|
2340
|
+
if (result) return asJsonResponse({
|
|
2341
|
+
success: true,
|
|
2342
|
+
strategy: "frida-injection",
|
|
2343
|
+
result
|
|
2344
|
+
});
|
|
2345
|
+
} catch {}
|
|
2346
|
+
return asJsonResponse({
|
|
2347
|
+
success: true,
|
|
2348
|
+
strategy: "manual-bypass",
|
|
2349
|
+
instructions: {
|
|
2350
|
+
android: ["Use Frida to hook X509TrustManager.checkServerTrusted and return without throwing.", "Alternatively, use OkHttp CertificatePinner.Builder().add() with the target cert."],
|
|
2351
|
+
ios: ["Hook SecTrustEvaluateWithError to always return true.", "Or use SSLSetSessionOption to disable certificate validation."],
|
|
2352
|
+
desktop: ["Set NODE_TLS_REJECT_UNAUTHORIZED=0 for Node.js targets.", "Or patch the certificate comparison function in the HTTP client."]
|
|
2353
|
+
},
|
|
2354
|
+
args
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
async handleRawTcpSend(args) {
|
|
2358
|
+
const host = argString(args, "host") ?? "127.0.0.1";
|
|
2359
|
+
const port = argNumber(args, "port");
|
|
2360
|
+
if (port === void 0 || port < 1 || port > 65535) return {
|
|
2361
|
+
ok: false,
|
|
2362
|
+
error: "port must be a number between 1 and 65535"
|
|
2363
|
+
};
|
|
2364
|
+
const ssrfCheck = validateNetworkTarget(host);
|
|
2365
|
+
if (ssrfCheck) return ssrfCheck;
|
|
2366
|
+
const dataHex = argString(args, "dataHex");
|
|
2367
|
+
const dataText = argString(args, "dataText");
|
|
2368
|
+
if (!dataHex && !dataText) return {
|
|
2369
|
+
ok: false,
|
|
2370
|
+
error: "dataHex or dataText is required"
|
|
2371
|
+
};
|
|
2372
|
+
const data = dataHex ? Buffer.from(normalizeHex(dataHex), "hex") : Buffer.from(dataText ?? "", "utf8");
|
|
2373
|
+
const timeout = argNumber(args, "timeout") ?? 5e3;
|
|
2374
|
+
return new Promise((resolve) => {
|
|
2375
|
+
const socket = new Socket();
|
|
2376
|
+
const timer = setTimeout(() => {
|
|
2377
|
+
socket.destroy();
|
|
2378
|
+
resolve({
|
|
2379
|
+
ok: false,
|
|
2380
|
+
error: "Connection timed out"
|
|
2381
|
+
});
|
|
2382
|
+
}, timeout);
|
|
2383
|
+
socket.on("connect", () => {
|
|
2384
|
+
socket.write(data, () => {
|
|
2385
|
+
socket.end();
|
|
2386
|
+
});
|
|
2387
|
+
});
|
|
2388
|
+
socket.on("data", (chunk) => {
|
|
2389
|
+
clearTimeout(timer);
|
|
2390
|
+
resolve({
|
|
2391
|
+
ok: true,
|
|
2392
|
+
host,
|
|
2393
|
+
port,
|
|
2394
|
+
sentBytes: data.length,
|
|
2395
|
+
responseHex: chunk.toString("hex").toUpperCase(),
|
|
2396
|
+
responseText: chunk.toString("utf8")
|
|
2397
|
+
});
|
|
2398
|
+
socket.destroy();
|
|
2399
|
+
});
|
|
2400
|
+
socket.on("error", (error) => {
|
|
2401
|
+
clearTimeout(timer);
|
|
2402
|
+
resolve({
|
|
2403
|
+
ok: false,
|
|
2404
|
+
error: error.message
|
|
2405
|
+
});
|
|
2406
|
+
});
|
|
2407
|
+
socket.connect(port, host);
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
async handleRawTcpListen(args) {
|
|
2411
|
+
const port = argNumber(args, "port");
|
|
2412
|
+
if (port === void 0 || port < 1 || port > 65535) return {
|
|
2413
|
+
ok: false,
|
|
2414
|
+
error: "port must be a number between 1 and 65535"
|
|
2415
|
+
};
|
|
2416
|
+
const timeout = argNumber(args, "timeout") ?? 1e4;
|
|
2417
|
+
return new Promise((resolve) => {
|
|
2418
|
+
const server = createServer();
|
|
2419
|
+
const timer = setTimeout(() => {
|
|
2420
|
+
server.close();
|
|
2421
|
+
resolve({
|
|
2422
|
+
ok: false,
|
|
2423
|
+
error: "Listen timed out — no connection received"
|
|
2424
|
+
});
|
|
2425
|
+
}, timeout);
|
|
2426
|
+
server.on("connection", (socket) => {
|
|
2427
|
+
clearTimeout(timer);
|
|
2428
|
+
const chunks = [];
|
|
2429
|
+
socket.on("data", (chunk) => {
|
|
2430
|
+
chunks.push(chunk);
|
|
2431
|
+
});
|
|
2432
|
+
socket.on("end", () => {
|
|
2433
|
+
const data = Buffer.concat(chunks);
|
|
2434
|
+
server.close();
|
|
2435
|
+
resolve({
|
|
2436
|
+
ok: true,
|
|
2437
|
+
port,
|
|
2438
|
+
receivedBytes: data.length,
|
|
2439
|
+
dataHex: data.toString("hex").toUpperCase(),
|
|
2440
|
+
dataText: data.toString("utf8")
|
|
2441
|
+
});
|
|
2442
|
+
});
|
|
2443
|
+
socket.on("error", (error) => {
|
|
2444
|
+
clearTimeout(timer);
|
|
2445
|
+
server.close();
|
|
2446
|
+
resolve({
|
|
2447
|
+
ok: false,
|
|
2448
|
+
error: error.message
|
|
2449
|
+
});
|
|
2450
|
+
});
|
|
2451
|
+
});
|
|
2452
|
+
server.on("error", (error) => {
|
|
2453
|
+
clearTimeout(timer);
|
|
2454
|
+
resolve({
|
|
2455
|
+
ok: false,
|
|
2456
|
+
error: error.message
|
|
2457
|
+
});
|
|
2458
|
+
});
|
|
2459
|
+
server.listen(port, "127.0.0.1");
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
async handleRawUdpSend(args) {
|
|
2463
|
+
const host = argString(args, "host") ?? "127.0.0.1";
|
|
2464
|
+
const port = argNumber(args, "port");
|
|
2465
|
+
if (port === void 0 || port < 1 || port > 65535) return {
|
|
2466
|
+
ok: false,
|
|
2467
|
+
error: "port must be a number between 1 and 65535"
|
|
2468
|
+
};
|
|
2469
|
+
const ssrfCheck = validateNetworkTarget(host);
|
|
2470
|
+
if (ssrfCheck) return ssrfCheck;
|
|
2471
|
+
const dataHex = argString(args, "dataHex");
|
|
2472
|
+
const dataText = argString(args, "dataText");
|
|
2473
|
+
if (!dataHex && !dataText) return {
|
|
2474
|
+
ok: false,
|
|
2475
|
+
error: "dataHex or dataText is required"
|
|
2476
|
+
};
|
|
2477
|
+
const data = dataHex ? Buffer.from(normalizeHex(dataHex), "hex") : Buffer.from(dataText ?? "", "utf8");
|
|
2478
|
+
const timeout = argNumber(args, "timeout") ?? 5e3;
|
|
2479
|
+
return new Promise((resolve) => {
|
|
2480
|
+
const socket = createSocket("udp4");
|
|
2481
|
+
const timer = setTimeout(() => {
|
|
2482
|
+
socket.close();
|
|
2483
|
+
resolve({
|
|
2484
|
+
ok: false,
|
|
2485
|
+
error: "UDP response timed out"
|
|
2486
|
+
});
|
|
2487
|
+
}, timeout);
|
|
2488
|
+
socket.on("message", (msg) => {
|
|
2489
|
+
clearTimeout(timer);
|
|
2490
|
+
socket.close();
|
|
2491
|
+
resolve({
|
|
2492
|
+
ok: true,
|
|
2493
|
+
host,
|
|
2494
|
+
port,
|
|
2495
|
+
sentBytes: data.length,
|
|
2496
|
+
responseHex: msg.toString("hex").toUpperCase(),
|
|
2497
|
+
responseText: msg.toString("utf8")
|
|
2498
|
+
});
|
|
2499
|
+
});
|
|
2500
|
+
socket.on("error", (error) => {
|
|
2501
|
+
clearTimeout(timer);
|
|
2502
|
+
socket.close();
|
|
2503
|
+
resolve({
|
|
2504
|
+
ok: false,
|
|
2505
|
+
error: error.message
|
|
2506
|
+
});
|
|
2507
|
+
});
|
|
2508
|
+
socket.send(data, 0, data.length, port, host);
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
async handleRawUdpListen(args) {
|
|
2512
|
+
const port = argNumber(args, "port");
|
|
2513
|
+
if (port === void 0 || port < 1 || port > 65535) return {
|
|
2514
|
+
ok: false,
|
|
2515
|
+
error: "port must be a number between 1 and 65535"
|
|
2516
|
+
};
|
|
2517
|
+
const timeout = argNumber(args, "timeout") ?? 1e4;
|
|
2518
|
+
return new Promise((resolve) => {
|
|
2519
|
+
const socket = createSocket("udp4");
|
|
2520
|
+
const timer = setTimeout(() => {
|
|
2521
|
+
socket.close();
|
|
2522
|
+
resolve({
|
|
2523
|
+
ok: false,
|
|
2524
|
+
error: "UDP listen timed out"
|
|
2525
|
+
});
|
|
2526
|
+
}, timeout);
|
|
2527
|
+
socket.on("message", (msg, rinfo) => {
|
|
2528
|
+
clearTimeout(timer);
|
|
2529
|
+
socket.close();
|
|
2530
|
+
resolve({
|
|
2531
|
+
ok: true,
|
|
2532
|
+
localPort: port,
|
|
2533
|
+
receivedBytes: msg.length,
|
|
2534
|
+
from: rinfo,
|
|
2535
|
+
dataHex: msg.toString("hex").toUpperCase(),
|
|
2536
|
+
dataText: msg.toString("utf8")
|
|
2537
|
+
});
|
|
2538
|
+
});
|
|
2539
|
+
socket.on("error", (error) => {
|
|
2540
|
+
clearTimeout(timer);
|
|
2541
|
+
socket.close();
|
|
2542
|
+
resolve({
|
|
2543
|
+
ok: false,
|
|
2544
|
+
error: error.message
|
|
2545
|
+
});
|
|
2546
|
+
});
|
|
2547
|
+
socket.bind(port, "127.0.0.1");
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
//#endregion
|
|
2552
|
+
export { BoringsslInspectorHandlers };
|