@calltelemetry/cucm-mcp 0.1.7 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -7
- package/dist/axl.js +235 -0
- package/dist/axl.js.map +7 -0
- package/dist/dime.js +231 -269
- package/dist/dime.js.map +7 -0
- package/dist/index.js +680 -207
- package/dist/index.js.map +7 -0
- package/dist/multipart.js +58 -68
- package/dist/multipart.js.map +7 -0
- package/dist/packetCapture.js +315 -373
- package/dist/packetCapture.js.map +7 -0
- package/dist/pcap-analyze.js +601 -0
- package/dist/pcap-analyze.js.map +7 -0
- package/dist/state.js +105 -104
- package/dist/state.js.map +7 -0
- package/dist/time.js +25 -23
- package/dist/time.js.map +7 -0
- package/package.json +4 -6
package/dist/packetCapture.js
CHANGED
|
@@ -2,398 +2,340 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { Client } from "ssh2";
|
|
3
3
|
import { defaultStateStore } from "./state.js";
|
|
4
4
|
function extractCapFilePath(text) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const matches = text.match(re);
|
|
11
|
-
if (!matches || matches.length === 0)
|
|
12
|
-
return undefined;
|
|
13
|
-
return matches[matches.length - 1];
|
|
5
|
+
if (!text) return void 0;
|
|
6
|
+
const re = /\/var\/log\/active\/platform\/cli\/[A-Za-z0-9._-]+\.cap/g;
|
|
7
|
+
const matches = text.match(re);
|
|
8
|
+
if (!matches || matches.length === 0) return void 0;
|
|
9
|
+
return matches[matches.length - 1];
|
|
14
10
|
}
|
|
15
11
|
function looksLikeCucmPrompt(text) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Some environments might show other usernames.
|
|
20
|
-
// We only look at the tail to avoid false positives.
|
|
21
|
-
const tail = t.slice(-80);
|
|
22
|
-
return /(?:^|\n)[A-Za-z0-9_-]+:\s*$/.test(tail);
|
|
12
|
+
const t = String(text || "");
|
|
13
|
+
const tail = t.slice(-80);
|
|
14
|
+
return /(?:^|\n)[A-Za-z0-9_-]+:\s*$/.test(tail);
|
|
23
15
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
function resolveSshAuth(auth) {
|
|
17
|
+
const username = auth?.username || process.env.CUCM_SSH_USERNAME;
|
|
18
|
+
const password = auth?.password || process.env.CUCM_SSH_PASSWORD;
|
|
19
|
+
if (!username || !password) {
|
|
20
|
+
throw new Error("Missing SSH credentials (provide auth or set CUCM_SSH_USERNAME/CUCM_SSH_PASSWORD)");
|
|
21
|
+
}
|
|
22
|
+
return { username, password };
|
|
31
23
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.trim()
|
|
37
|
-
.replaceAll(".", "_")
|
|
38
|
-
.replace(/[^A-Za-z0-9_-]+/g, "_")
|
|
39
|
-
.replace(/^_+/, "")
|
|
40
|
-
.replace(/_+$/, "");
|
|
41
|
-
if (!cleaned)
|
|
42
|
-
throw new Error("fileBase is required (after sanitization)");
|
|
43
|
-
return cleaned;
|
|
24
|
+
function sanitizeFileBase(s) {
|
|
25
|
+
const cleaned = String(s || "").trim().replaceAll(".", "_").replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+/, "").replace(/_+$/, "");
|
|
26
|
+
if (!cleaned) throw new Error("fileBase is required (after sanitization)");
|
|
27
|
+
return cleaned;
|
|
44
28
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
args.push("host", "ip", ip);
|
|
75
|
-
}
|
|
76
|
-
return args.join(" ");
|
|
29
|
+
function buildCaptureCommand(opts) {
|
|
30
|
+
const iface = String(opts.iface || "eth0").trim() || "eth0";
|
|
31
|
+
const fileBase = sanitizeFileBase(opts.fileBase);
|
|
32
|
+
const count = Number.isFinite(opts.count) ? Math.trunc(opts.count) : 1e5;
|
|
33
|
+
if (count <= 0) throw new Error("count must be > 0");
|
|
34
|
+
const size = String(opts.size || "all").trim() || "all";
|
|
35
|
+
const args = [
|
|
36
|
+
"utils",
|
|
37
|
+
"network",
|
|
38
|
+
"capture",
|
|
39
|
+
iface,
|
|
40
|
+
"file",
|
|
41
|
+
fileBase,
|
|
42
|
+
"count",
|
|
43
|
+
String(count),
|
|
44
|
+
"size",
|
|
45
|
+
size
|
|
46
|
+
];
|
|
47
|
+
if (opts.portFilter != null) {
|
|
48
|
+
const p = Math.trunc(opts.portFilter);
|
|
49
|
+
if (p < 1 || p > 65535) throw new Error("portFilter must be 1..65535");
|
|
50
|
+
args.push("port", String(p));
|
|
51
|
+
}
|
|
52
|
+
if (opts.hostFilterIp) {
|
|
53
|
+
const ip = String(opts.hostFilterIp).trim();
|
|
54
|
+
if (!ip) throw new Error("hostFilterIp must be non-empty");
|
|
55
|
+
args.push("host", "ip", ip);
|
|
56
|
+
}
|
|
57
|
+
return args.join(" ");
|
|
77
58
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
function remoteCapturePath(fileBase) {
|
|
60
|
+
const fb = sanitizeFileBase(fileBase);
|
|
61
|
+
return `/var/log/active/platform/cli/${fb}.cap`;
|
|
81
62
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return out;
|
|
63
|
+
function remoteCaptureCandidates(fileBase, maxParts = 10) {
|
|
64
|
+
const fb = sanitizeFileBase(fileBase);
|
|
65
|
+
const base = `/var/log/active/platform/cli/${fb}.cap`;
|
|
66
|
+
const out = [base];
|
|
67
|
+
for (let i = 1; i <= maxParts; i++) {
|
|
68
|
+
const suffix = String(i).padStart(2, "0");
|
|
69
|
+
out.push(`/var/log/active/platform/cli/${fb}.cap${suffix}`);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
92
72
|
}
|
|
93
73
|
function waitFor(channel, session, predicate, timeoutMs, timeoutMessage) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
channel.stderr.on("data", onData);
|
|
116
|
-
timer = setTimeout(() => cleanup(false), Math.max(1000, timeoutMs));
|
|
117
|
-
timer.unref?.();
|
|
118
|
-
});
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (predicate()) return resolve();
|
|
76
|
+
const onData = () => {
|
|
77
|
+
if (predicate()) cleanup(true);
|
|
78
|
+
};
|
|
79
|
+
let timer;
|
|
80
|
+
const cleanup = (ok) => {
|
|
81
|
+
channel.off("data", onData);
|
|
82
|
+
channel.stderr.off("data", onData);
|
|
83
|
+
if (timer) clearTimeout(timer);
|
|
84
|
+
if (ok) resolve();
|
|
85
|
+
else {
|
|
86
|
+
const tail = (session.lastStdout || session.lastStderr || "").slice(-400);
|
|
87
|
+
reject(new Error(`${timeoutMessage}. lastOutput=${JSON.stringify(tail)}`));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
channel.on("data", onData);
|
|
91
|
+
channel.stderr.on("data", onData);
|
|
92
|
+
timer = setTimeout(() => cleanup(false), Math.max(1e3, timeoutMs));
|
|
93
|
+
timer.unref?.();
|
|
94
|
+
});
|
|
119
95
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
96
|
+
class PacketCaptureManager {
|
|
97
|
+
active = /* @__PURE__ */ new Map();
|
|
98
|
+
state;
|
|
99
|
+
constructor(opts) {
|
|
100
|
+
this.state = opts?.state || defaultStateStore();
|
|
101
|
+
}
|
|
102
|
+
list() {
|
|
103
|
+
return [...this.active.values()].map((a) => a.session);
|
|
104
|
+
}
|
|
105
|
+
async start(opts) {
|
|
106
|
+
const iface = String(opts.iface || "eth0").trim() || "eth0";
|
|
107
|
+
const fileBase = sanitizeFileBase(opts.fileBase || `cap_${Date.now()}`);
|
|
108
|
+
const count = opts.count ?? 1e5;
|
|
109
|
+
const size = opts.size ?? "all";
|
|
110
|
+
const cmd = buildCaptureCommand({ iface, fileBase, count, size, hostFilterIp: opts.hostFilterIp, portFilter: opts.portFilter });
|
|
111
|
+
const id = crypto.randomUUID();
|
|
112
|
+
const sshPort = opts.sshPort ?? (process.env.CUCM_SSH_PORT ? Number.parseInt(process.env.CUCM_SSH_PORT, 10) : 22);
|
|
113
|
+
const auth = resolveSshAuth(opts.auth);
|
|
114
|
+
const client = new Client();
|
|
115
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
116
|
+
const session = {
|
|
117
|
+
id,
|
|
118
|
+
host: opts.host,
|
|
119
|
+
startedAt,
|
|
120
|
+
iface,
|
|
121
|
+
fileBase,
|
|
122
|
+
remoteFilePath: remoteCapturePath(fileBase),
|
|
123
|
+
remoteFileCandidates: remoteCaptureCandidates(fileBase)
|
|
124
|
+
};
|
|
125
|
+
this.state.upsert({
|
|
126
|
+
...session,
|
|
127
|
+
stoppedAt: session.stoppedAt
|
|
128
|
+
});
|
|
129
|
+
const startTimeoutMs = Math.max(2e3, Math.trunc(opts.startTimeoutMs ?? 3e4));
|
|
130
|
+
let connectTimer;
|
|
131
|
+
const connectTimeout = new Promise((_, reject) => {
|
|
132
|
+
connectTimer = setTimeout(() => reject(new Error(`SSH connect timed out after ${startTimeoutMs}ms`)), startTimeoutMs);
|
|
133
|
+
connectTimer.unref?.();
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
await Promise.race([
|
|
137
|
+
new Promise((resolve, reject) => {
|
|
138
|
+
client.on("ready", () => resolve()).on("error", (e) => reject(e)).connect({
|
|
142
139
|
host: opts.host,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// ignore
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
// Wait for prompt before running capture command.
|
|
198
|
-
// Some CUCM shells don't print a prompt until you send a newline.
|
|
199
|
-
const promptTimeoutMs = Math.max(2000, Math.trunc(opts.startTimeoutMs ?? 30_000));
|
|
200
|
-
try {
|
|
201
|
-
try {
|
|
202
|
-
channel.write("\n");
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
// ignore
|
|
206
|
-
}
|
|
207
|
-
// Nudge a couple times if no prompt yet.
|
|
208
|
-
void (async () => {
|
|
209
|
-
const delays = [400, 1200, 2500];
|
|
210
|
-
for (const d of delays) {
|
|
211
|
-
await new Promise((r) => setTimeout(r, d));
|
|
212
|
-
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr))
|
|
213
|
-
return;
|
|
214
|
-
try {
|
|
215
|
-
channel.write("\n");
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
// ignore
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
})();
|
|
222
|
-
await waitFor(channel, session, () => looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr), promptTimeoutMs, "Timeout waiting for CUCM CLI prompt");
|
|
223
|
-
channel.write(`${cmd}\n`);
|
|
224
|
-
}
|
|
225
|
-
catch (e) {
|
|
226
|
-
try {
|
|
227
|
-
channel.end();
|
|
228
|
-
channel.close();
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
// ignore
|
|
232
|
-
}
|
|
233
|
-
try {
|
|
234
|
-
client.end();
|
|
235
|
-
client.destroy();
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// ignore
|
|
239
|
-
}
|
|
240
|
-
throw e;
|
|
241
|
-
}
|
|
242
|
-
this.active.set(id, { session, client, channel });
|
|
243
|
-
// Optional guard: stop after a duration even if packet count isn't reached.
|
|
244
|
-
if (opts.maxDurationMs != null) {
|
|
245
|
-
const dur = Math.max(250, Math.trunc(opts.maxDurationMs));
|
|
246
|
-
const t = setTimeout(() => {
|
|
247
|
-
void this.stop(id).catch(() => {
|
|
248
|
-
// ignore
|
|
249
|
-
});
|
|
250
|
-
}, dur);
|
|
251
|
-
t.unref?.();
|
|
140
|
+
port: sshPort,
|
|
141
|
+
username: auth.username,
|
|
142
|
+
password: auth.password,
|
|
143
|
+
readyTimeout: 15e3
|
|
144
|
+
});
|
|
145
|
+
}),
|
|
146
|
+
connectTimeout
|
|
147
|
+
]);
|
|
148
|
+
} finally {
|
|
149
|
+
if (connectTimer) clearTimeout(connectTimer);
|
|
150
|
+
}
|
|
151
|
+
const channel = await new Promise((resolve, reject) => {
|
|
152
|
+
client.shell({ term: "vt100", cols: 120, rows: 40 }, (err, ch) => {
|
|
153
|
+
if (err) return reject(err);
|
|
154
|
+
resolve(ch);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
channel.on("data", (buf) => {
|
|
158
|
+
session.lastStdout = buf.toString("utf8").slice(-2e3);
|
|
159
|
+
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
160
|
+
});
|
|
161
|
+
channel.stderr.on("data", (buf) => {
|
|
162
|
+
session.lastStderr = buf.toString("utf8").slice(-2e3);
|
|
163
|
+
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
164
|
+
});
|
|
165
|
+
channel.on("close", (code) => {
|
|
166
|
+
session.exitCode = code;
|
|
167
|
+
session.stoppedAt = session.stoppedAt || (/* @__PURE__ */ new Date()).toISOString();
|
|
168
|
+
this.active.delete(id);
|
|
169
|
+
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
170
|
+
try {
|
|
171
|
+
client.end();
|
|
172
|
+
client.destroy();
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
const promptTimeoutMs = startTimeoutMs;
|
|
177
|
+
try {
|
|
178
|
+
try {
|
|
179
|
+
channel.write("\n");
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
void (async () => {
|
|
183
|
+
const delays = [400, 1200, 2500];
|
|
184
|
+
for (const d of delays) {
|
|
185
|
+
await new Promise((r) => setTimeout(r, d));
|
|
186
|
+
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) return;
|
|
187
|
+
try {
|
|
188
|
+
channel.write("\n");
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
252
191
|
}
|
|
253
|
-
|
|
192
|
+
})();
|
|
193
|
+
await waitFor(
|
|
194
|
+
channel,
|
|
195
|
+
session,
|
|
196
|
+
() => looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr),
|
|
197
|
+
promptTimeoutMs,
|
|
198
|
+
"Timeout waiting for CUCM CLI prompt"
|
|
199
|
+
);
|
|
200
|
+
channel.write(`${cmd}
|
|
201
|
+
`);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
try {
|
|
204
|
+
channel.end();
|
|
205
|
+
channel.close();
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
client.end();
|
|
210
|
+
client.destroy();
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
throw e;
|
|
254
214
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const done = new Promise((resolve) => {
|
|
261
|
-
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) {
|
|
262
|
-
return resolve();
|
|
263
|
-
}
|
|
264
|
-
// CUCM CLI can keep the SSH channel open after the command finishes
|
|
265
|
-
// (it returns to a prompt rather than exiting). Treat prompt as "stopped".
|
|
266
|
-
const onData = () => {
|
|
267
|
-
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) {
|
|
268
|
-
cleanup();
|
|
269
|
-
resolve();
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
const cleanup = () => {
|
|
273
|
-
channel.off("data", onData);
|
|
274
|
-
channel.stderr.off("data", onData);
|
|
275
|
-
};
|
|
276
|
-
const finish = () => {
|
|
277
|
-
cleanup();
|
|
278
|
-
resolve();
|
|
279
|
-
};
|
|
280
|
-
channel.once("exit", finish);
|
|
281
|
-
channel.once("close", finish);
|
|
282
|
-
channel.once("end", finish);
|
|
283
|
-
channel.on("data", onData);
|
|
284
|
-
channel.stderr.on("data", onData);
|
|
215
|
+
this.active.set(id, { session, client, channel });
|
|
216
|
+
if (opts.maxDurationMs != null) {
|
|
217
|
+
const dur = Math.max(250, Math.trunc(opts.maxDurationMs));
|
|
218
|
+
const t = setTimeout(() => {
|
|
219
|
+
void this.stop(id).catch(() => {
|
|
285
220
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
// CUCM can take a while to flush/close captures (especially big buffers).
|
|
304
|
-
// Also, some CLI flows require a second Ctrl-C or a newline to return to prompt.
|
|
305
|
-
const resolvedTimeoutMs = Math.max(5000, timeoutMs || 0);
|
|
306
|
-
const deadline = Date.now() + resolvedTimeoutMs;
|
|
307
|
-
sendInterrupt();
|
|
308
|
-
// Nudge again a couple times if it doesn't exit quickly.
|
|
309
|
-
void (async () => {
|
|
310
|
-
const delays = [750, 1500, 3000];
|
|
311
|
-
for (const d of delays) {
|
|
312
|
-
await new Promise((r) => setTimeout(r, d));
|
|
313
|
-
if (Date.now() >= deadline)
|
|
314
|
-
return;
|
|
315
|
-
// If channel already closed, no-op.
|
|
316
|
-
sendInterrupt();
|
|
317
|
-
try {
|
|
318
|
-
channel.write("\n");
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// ignore
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
})();
|
|
325
|
-
try {
|
|
326
|
-
let timer;
|
|
327
|
-
const timeout = new Promise((_, reject) => {
|
|
328
|
-
timer = setTimeout(() => reject(new Error("Timeout waiting for capture to stop")), resolvedTimeoutMs);
|
|
329
|
-
// Don't keep the process alive just because we're waiting.
|
|
330
|
-
timer.unref?.();
|
|
331
|
-
});
|
|
332
|
-
try {
|
|
333
|
-
await Promise.race([done, timeout]);
|
|
334
|
-
}
|
|
335
|
-
finally {
|
|
336
|
-
if (timer)
|
|
337
|
-
clearTimeout(timer);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
catch (e) {
|
|
341
|
-
// Don't hard-fail: if the CLI doesn't emit a prompt/exit, we still want to:
|
|
342
|
-
// - close SSH resources
|
|
343
|
-
// - let the caller try DIME downloads (.cap, .cap01, etc)
|
|
344
|
-
session.stopTimedOut = true;
|
|
345
|
-
session.stoppedAt = session.stoppedAt || new Date().toISOString();
|
|
346
|
-
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
347
|
-
try {
|
|
348
|
-
channel.close();
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// ignore
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
session.stoppedAt = new Date().toISOString();
|
|
355
|
-
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
356
|
-
// If we're back at a CUCM prompt, try to close the session cleanly.
|
|
221
|
+
}, dur);
|
|
222
|
+
t.unref?.();
|
|
223
|
+
}
|
|
224
|
+
return session;
|
|
225
|
+
}
|
|
226
|
+
async stop(captureId, timeoutMs = 9e4) {
|
|
227
|
+
const a = this.active.get(captureId);
|
|
228
|
+
if (!a) throw new Error(`Unknown captureId: ${captureId}`);
|
|
229
|
+
const { channel, client, session } = a;
|
|
230
|
+
const done = new Promise((resolve) => {
|
|
231
|
+
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) {
|
|
232
|
+
return resolve();
|
|
233
|
+
}
|
|
234
|
+
const onData = () => {
|
|
357
235
|
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
// ignore
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
// Ensure the channel is closed so the SSH client can terminate promptly.
|
|
366
|
-
try {
|
|
367
|
-
channel.end();
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
// ignore
|
|
236
|
+
cleanup();
|
|
237
|
+
resolve();
|
|
371
238
|
}
|
|
239
|
+
};
|
|
240
|
+
const cleanup = () => {
|
|
241
|
+
channel.off("data", onData);
|
|
242
|
+
channel.stderr.off("data", onData);
|
|
243
|
+
};
|
|
244
|
+
const finish = () => {
|
|
245
|
+
cleanup();
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
channel.once("exit", finish);
|
|
249
|
+
channel.once("close", finish);
|
|
250
|
+
channel.once("end", finish);
|
|
251
|
+
channel.on("data", onData);
|
|
252
|
+
channel.stderr.on("data", onData);
|
|
253
|
+
});
|
|
254
|
+
const sendInterrupt = () => {
|
|
255
|
+
try {
|
|
256
|
+
if (typeof channel.signal === "function") channel.signal("INT");
|
|
257
|
+
channel.write("");
|
|
258
|
+
} catch {
|
|
372
259
|
try {
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
// ignore
|
|
377
|
-
}
|
|
378
|
-
// Attempt to learn the actual remote file path from CLI output.
|
|
379
|
-
// This helps when CUCM appends suffixes or reports a different on-disk location.
|
|
380
|
-
const inferred = extractCapFilePath(session.lastStdout) || extractCapFilePath(session.lastStderr);
|
|
381
|
-
if (inferred) {
|
|
382
|
-
session.remoteFilePath = inferred;
|
|
383
|
-
if (!session.remoteFileCandidates.includes(inferred)) {
|
|
384
|
-
session.remoteFileCandidates = [inferred, ...session.remoteFileCandidates];
|
|
385
|
-
}
|
|
260
|
+
channel.write("");
|
|
261
|
+
} catch {
|
|
386
262
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const resolvedTimeoutMs = Math.max(5e3, timeoutMs || 0);
|
|
266
|
+
const deadline = Date.now() + resolvedTimeoutMs;
|
|
267
|
+
sendInterrupt();
|
|
268
|
+
void (async () => {
|
|
269
|
+
const delays = [750, 1500, 3e3];
|
|
270
|
+
for (const d of delays) {
|
|
271
|
+
await new Promise((r) => setTimeout(r, d));
|
|
272
|
+
if (Date.now() >= deadline) return;
|
|
273
|
+
sendInterrupt();
|
|
390
274
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
// ignore
|
|
275
|
+
channel.write("\n");
|
|
276
|
+
} catch {
|
|
396
277
|
}
|
|
397
|
-
|
|
278
|
+
}
|
|
279
|
+
})();
|
|
280
|
+
try {
|
|
281
|
+
let timer;
|
|
282
|
+
const timeout = new Promise((_, reject) => {
|
|
283
|
+
timer = setTimeout(() => reject(new Error("Timeout waiting for capture to stop")), resolvedTimeoutMs);
|
|
284
|
+
timer.unref?.();
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
await Promise.race([done, timeout]);
|
|
288
|
+
} finally {
|
|
289
|
+
if (timer) clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
session.stopTimedOut = true;
|
|
293
|
+
session.stoppedAt = session.stoppedAt || (/* @__PURE__ */ new Date()).toISOString();
|
|
294
|
+
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
295
|
+
try {
|
|
296
|
+
channel.close();
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
301
|
+
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
302
|
+
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr)) {
|
|
303
|
+
try {
|
|
304
|
+
channel.write("exit\n");
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
channel.end();
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
channel.close();
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
const inferred = extractCapFilePath(session.lastStdout) || extractCapFilePath(session.lastStderr);
|
|
317
|
+
if (inferred) {
|
|
318
|
+
session.remoteFilePath = inferred;
|
|
319
|
+
if (!session.remoteFileCandidates.includes(inferred)) {
|
|
320
|
+
session.remoteFileCandidates = [inferred, ...session.remoteFileCandidates];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.state.upsert({ ...session, stoppedAt: session.stoppedAt });
|
|
324
|
+
this.active.delete(captureId);
|
|
325
|
+
try {
|
|
326
|
+
client.end();
|
|
327
|
+
client.destroy();
|
|
328
|
+
} catch {
|
|
398
329
|
}
|
|
330
|
+
return session;
|
|
331
|
+
}
|
|
399
332
|
}
|
|
333
|
+
export {
|
|
334
|
+
PacketCaptureManager,
|
|
335
|
+
buildCaptureCommand,
|
|
336
|
+
remoteCaptureCandidates,
|
|
337
|
+
remoteCapturePath,
|
|
338
|
+
resolveSshAuth,
|
|
339
|
+
sanitizeFileBase
|
|
340
|
+
};
|
|
341
|
+
//# sourceMappingURL=packetCapture.js.map
|