@jshookmcp/jshook 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +36 -5
  2. package/README.zh.md +36 -5
  3. package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-BNk-EoBt.mjs} +3 -3
  4. package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-Cq8q01kp.mjs} +5 -5
  5. package/dist/ConsoleMonitor-CPVQW1Y-.mjs +2201 -0
  6. package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-BNPxu0RH.mjs} +1 -1
  7. package/dist/DetailedDataManager-BQQcxh64.mjs +217 -0
  8. package/dist/EventBus-DgPmwpeu.mjs +141 -0
  9. package/dist/EvidenceGraphBridge-SFesNera.mjs +153 -0
  10. package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-CWYgw0YW.mjs} +13 -6
  11. package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-gzWtkKuf.mjs} +1 -1
  12. package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-B9gZCdFP.mjs} +3 -3
  13. package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-BLDH0dCv.mjs} +4 -4
  14. package/dist/HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs +639 -0
  15. package/dist/InstrumentationSession-CvPC7Jwy.mjs +244 -0
  16. package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CbVdCIJF.mjs} +3 -3
  17. package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-BsDZbLYm.mjs} +81 -78
  18. package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-Bcpml6II.mjs} +44 -18
  19. package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-dZtA1ZGn.mjs} +14 -53
  20. package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-B-FjA2mJ.mjs} +1 -1
  21. package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-D1lzJ_VG.mjs} +2 -2
  22. package/dist/PageController-Bqm2kZ_X.mjs +417 -0
  23. package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-BOhyVsjx.mjs} +4 -4
  24. package/dist/PrerequisiteError-Dl33Svkz.mjs +20 -0
  25. package/dist/ResponseBuilder-D3iFYx2N.mjs +143 -0
  26. package/dist/ReverseEvidenceGraph-Dlsk94LC.mjs +269 -0
  27. package/dist/ScriptManager-aHHq0X7U.mjs +3000 -0
  28. package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-CqdIFlQl.mjs} +2 -2
  29. package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-DhFaPvRO.mjs} +3 -3
  30. package/dist/ToolCatalog-C0JGZoOm.mjs +582 -0
  31. package/dist/ToolError-jh9whhMd.mjs +15 -0
  32. package/dist/ToolProbe-oC7aPrkv.mjs +45 -0
  33. package/dist/ToolRegistry-BjaF4oNz.mjs +131 -0
  34. package/dist/ToolRouter.policy-BWV67ZK-.mjs +304 -0
  35. package/dist/TraceRecorder-DgxyVbdQ.mjs +519 -0
  36. package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-CePkipZY.mjs} +1 -1
  37. package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-BvKs-gxc.mjs} +2 -2
  38. package/dist/WorkflowEngine-CuvkZtWu.mjs +598 -0
  39. package/dist/analysis-CL9uACt9.mjs +463 -0
  40. package/dist/antidebug-CqDTB_uk.mjs +1081 -0
  41. package/dist/artifactRetention-CFEprwPw.mjs +591 -0
  42. package/dist/artifacts-Bk2-_uPq.mjs +59 -0
  43. package/dist/betterSqlite3-0pqusHHH.mjs +74 -0
  44. package/dist/binary-instrument-CXfpx6fT.mjs +979 -0
  45. package/dist/bind-helpers-xFfRF-qm.mjs +22 -0
  46. package/dist/boringssl-inspector-BH2D3VKc.mjs +180 -0
  47. package/dist/browser-BpOr5PEx.mjs +4082 -0
  48. package/dist/concurrency-Bt0yv1kJ.mjs +41 -0
  49. package/dist/{constants-CCvsN80K.mjs → constants-B0OANIBL.mjs} +88 -46
  50. package/dist/coordination-qUbyF8KU.mjs +259 -0
  51. package/dist/debugger-gnKxRSN0.mjs +1271 -0
  52. package/dist/definitions-6M-eejaT.mjs +53 -0
  53. package/dist/definitions-B18eyf0B.mjs +18 -0
  54. package/dist/definitions-B3QdlrHv.mjs +34 -0
  55. package/dist/definitions-B4rAvHNZ.mjs +63 -0
  56. package/dist/definitions-BB_4jnmy.mjs +37 -0
  57. package/dist/definitions-BMfYXoNC.mjs +43 -0
  58. package/dist/definitions-Beid2EB3.mjs +27 -0
  59. package/dist/definitions-C1UvM5Iy.mjs +126 -0
  60. package/dist/definitions-CXEI7QC72.mjs +216 -0
  61. package/dist/definitions-C_4r7Fo-2.mjs +14 -0
  62. package/dist/definitions-CkFDALoa.mjs +26 -0
  63. package/dist/definitions-Cke7zEb8.mjs +94 -0
  64. package/dist/definitions-ClJLzsJQ.mjs +25 -0
  65. package/dist/definitions-Cq-zroAU.mjs +28 -0
  66. package/dist/definitions-Cy3Sl6gV.mjs +34 -0
  67. package/dist/definitions-D3VsGcvz.mjs +47 -0
  68. package/dist/definitions-DVGfrn7y.mjs +96 -0
  69. package/dist/definitions-LKpC3-nL.mjs +9 -0
  70. package/dist/definitions-bAhHQJq9.mjs +359 -0
  71. package/dist/encoding-Bvz5jLRv.mjs +1065 -0
  72. package/dist/evidence-graph-bridge-C_fv9PuC.mjs +135 -0
  73. package/dist/{factory-CibqTNC8.mjs → factory-DxlGh9Xf.mjs} +37 -52
  74. package/dist/graphql-DYWzJ29s.mjs +1026 -0
  75. package/dist/handlers-9sAbfIg-.mjs +2552 -0
  76. package/dist/handlers-Bl8zkwz1.mjs +2716 -0
  77. package/dist/handlers-C67ktuRN.mjs +710 -0
  78. package/dist/handlers-C87g8oCe.mjs +276 -0
  79. package/dist/handlers-CTsDAO6p.mjs +681 -0
  80. package/dist/handlers-Cgyg6c0U.mjs +645 -0
  81. package/dist/handlers-D6j6yka7.mjs +2124 -0
  82. package/dist/handlers-DdFzXLvF.mjs +446 -0
  83. package/dist/handlers-DeLOCd5m.mjs +799 -0
  84. package/dist/handlers-DlCJN4Td.mjs +757 -0
  85. package/dist/handlers-DxGIq15_2.mjs +917 -0
  86. package/dist/handlers-U6L4xhuF.mjs +585 -0
  87. package/dist/handlers-tB9Mp9ZK.mjs +84 -0
  88. package/dist/handlers-tiy7EIBp.mjs +572 -0
  89. package/dist/handlers.impl-DS0d9fUw.mjs +761 -0
  90. package/dist/hooks-CzCWByww.mjs +898 -0
  91. package/dist/index.mjs +377 -155
  92. package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
  93. package/dist/maintenance-P7ePRXQC.mjs +830 -0
  94. package/dist/manifest-2ToTpjv8.mjs +106 -0
  95. package/dist/manifest-3g71z6Bg.mjs +79 -0
  96. package/dist/manifest-82baTv4U.mjs +45 -0
  97. package/dist/manifest-B3QVVeBS.mjs +82 -0
  98. package/dist/manifest-BB2J8IMJ.mjs +149 -0
  99. package/dist/manifest-BKbgbSiY.mjs +60 -0
  100. package/dist/manifest-Bcf-TJzH.mjs +848 -0
  101. package/dist/manifest-BmtZzQiQ2.mjs +45 -0
  102. package/dist/manifest-Bnd7kqEY.mjs +55 -0
  103. package/dist/manifest-BqQX6OQC2.mjs +65 -0
  104. package/dist/manifest-BqrQ4Tpj.mjs +81 -0
  105. package/dist/manifest-Br4RPFt5.mjs +370 -0
  106. package/dist/manifest-C5qDjysN.mjs +107 -0
  107. package/dist/manifest-C9RT5nk32.mjs +34 -0
  108. package/dist/manifest-CAhOuvSl.mjs +204 -0
  109. package/dist/manifest-CBYWCUBJ.mjs +51 -0
  110. package/dist/manifest-CFADCRa1.mjs +37 -0
  111. package/dist/manifest-CQVhavRF.mjs +114 -0
  112. package/dist/manifest-CT7zZBV1.mjs +48 -0
  113. package/dist/manifest-CV12bcrF.mjs +121 -0
  114. package/dist/manifest-CXsRWjjI.mjs +224 -0
  115. package/dist/manifest-CZLUCfG02.mjs +95 -0
  116. package/dist/manifest-D6phHKFd.mjs +131 -0
  117. package/dist/manifest-DCyjf4n2.mjs +294 -0
  118. package/dist/manifest-DHsnKgP6.mjs +60 -0
  119. package/dist/manifest-Df_dliIe.mjs +55 -0
  120. package/dist/manifest-Dh8WBmEW.mjs +129 -0
  121. package/dist/manifest-DhKRAT8_.mjs +92 -0
  122. package/dist/manifest-DlpTj4ic2.mjs +193 -0
  123. package/dist/manifest-DrbmZcFl2.mjs +253 -0
  124. package/dist/manifest-DuwHjUa5.mjs +70 -0
  125. package/dist/manifest-DzwvxPJX.mjs +38 -0
  126. package/dist/manifest-NXctwWQq.mjs +68 -0
  127. package/dist/manifest-Sc_0JQ13.mjs +418 -0
  128. package/dist/manifest-gZ4s_UtG.mjs +96 -0
  129. package/dist/manifest-qSleDqdO.mjs +1023 -0
  130. package/dist/modules-C184v-S9.mjs +11365 -0
  131. package/dist/mojo-ipc-B_H61Afw.mjs +525 -0
  132. package/dist/network-671Cw6hV.mjs +3346 -0
  133. package/dist/{artifacts-BbdOMET5.mjs → outputPaths-B1uGmrWZ.mjs} +219 -212
  134. package/dist/parse-args-BlRjqlkL.mjs +39 -0
  135. package/dist/platform-WmNn8Sxb.mjs +2070 -0
  136. package/dist/process-QcbIy5Zq.mjs +1401 -0
  137. package/dist/proxy-DqNs0bAd.mjs +170 -0
  138. package/dist/registry-D-6e18lB.mjs +34 -0
  139. package/dist/response-BQVP-xUn.mjs +28 -0
  140. package/dist/server/plugin-api.mjs +2 -2
  141. package/dist/shared-state-board-DV-dpHFJ.mjs +586 -0
  142. package/dist/sourcemap-Dq8ez8vS.mjs +650 -0
  143. package/dist/ssrf-policy-ZaUfvhq7.mjs +166 -0
  144. package/dist/streaming-BUQ0VJsg.mjs +725 -0
  145. package/dist/tool-builder-DCbIC5Eo.mjs +186 -0
  146. package/dist/transform-CiYJfNX0.mjs +1007 -0
  147. package/dist/types-Bx92KJfT.mjs +4 -0
  148. package/dist/wasm-DQTnHDs4.mjs +531 -0
  149. package/dist/workflow-f3xJOcjx.mjs +725 -0
  150. package/package.json +16 -16
  151. package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
  152. package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
  153. package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-CDe5WPSV.mjs} +0 -0
  154. package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-Bo4T3bz8.mjs} +0 -0
  155. package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-CwVLVdDM.mjs} +0 -0
  156. package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-DVkj9kpI.mjs} +0 -0
  157. 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 };