fix: discover actual PCAP path via /file/print instead of guessing
After sniffer stops, poll /file/print every 500ms (up to 10s) until RouterOS reports the capture file, then use the exact 'name' field as the SFTP path. Falls back to stripping the leading slash if the first attempt gets "No such file". Eliminates the hardcoded path assumption. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6690ac9118
commit
a1788238f8
@@ -1788,29 +1788,39 @@ router.post('/:id/tools/capture', requireWrite, async (req: Request, res: Respon
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `cap-${Date.now()}`;
|
const fileName = `cap-${Date.now()}`;
|
||||||
// RouterOS SFTP root = device file system root; sniffer writes <file-name>.pcap there
|
|
||||||
const remotePath = `/${fileName}.pcap`;
|
|
||||||
const client = makeToolClient(device);
|
const client = makeToolClient(device);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[capture] connecting to device ${device.id} (${device.ip_address})`);
|
console.log(`[capture] connecting to device ${device.id} (${device.ip_address})`);
|
||||||
await client.connect();
|
await client.connect();
|
||||||
console.log(`[capture] connected; configuring sniffer`);
|
// Stop any in-progress capture before reconfiguring
|
||||||
// Stop any previous capture before reconfiguring
|
|
||||||
await client.execute('/tool/sniffer/stop').catch(() => {});
|
await client.execute('/tool/sniffer/stop').catch(() => {});
|
||||||
const setParams: Record<string, string> = { 'file-name': fileName, 'file-limit': '10240' };
|
const setParams: Record<string, string> = { 'file-name': fileName, 'file-limit': '10240' };
|
||||||
if (iface) setParams['filter-interface'] = iface;
|
if (iface) setParams['filter-interface'] = iface;
|
||||||
if (filter_ip) setParams['filter-ip-address'] = filter_ip;
|
if (filter_ip) setParams['filter-ip-address'] = filter_ip;
|
||||||
await client.execute('/tool/sniffer/set', setParams);
|
await client.execute('/tool/sniffer/set', setParams);
|
||||||
console.log(`[capture] sniffer configured; starting ${captureSec}s capture`);
|
console.log(`[capture] starting ${captureSec}s capture`);
|
||||||
await client.execute('/tool/sniffer/start');
|
await client.execute('/tool/sniffer/start');
|
||||||
await new Promise((r) => setTimeout(r, captureSec * 1000));
|
await new Promise((r) => setTimeout(r, captureSec * 1000));
|
||||||
await client.execute('/tool/sniffer/stop').catch(() => {});
|
await client.execute('/tool/sniffer/stop').catch(() => {});
|
||||||
|
|
||||||
|
// Poll /file/print until the capture file appears (up to 10s)
|
||||||
|
// RouterOS reports the exact SFTP-accessible path in the 'name' field
|
||||||
|
let remotePath: string | null = null;
|
||||||
|
for (let i = 0; i < 20 && !remotePath; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
const files = await client.execute('/file/print');
|
||||||
|
const hit = files.find((f) => (f['name'] ?? '').includes(fileName));
|
||||||
|
if (hit) remotePath = hit['name'];
|
||||||
|
}
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
console.log(`[capture] sniffer stopped; waiting for file flush`);
|
|
||||||
// Allow RouterOS a moment to flush the PCAP file before SFTP download
|
if (!remotePath) {
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
throw new Error(`Sniffer did not create file "${fileName}.pcap" — check device storage`);
|
||||||
console.log(`[capture] opening SFTP to ${device.ip_address}:${device.ssh_port ?? 22} path=${remotePath}`);
|
}
|
||||||
|
// RouterOS SFTP root is the device filesystem root; 'name' from /file/print is the path
|
||||||
|
const sftpPath = remotePath.startsWith('/') ? remotePath : `/${remotePath}`;
|
||||||
|
console.log(`[capture] file found: ${remotePath}; downloading via SFTP`);
|
||||||
|
|
||||||
// Download PCAP via SFTP then delete remote file
|
// Download PCAP via SFTP then delete remote file
|
||||||
const pcapBuffer = await new Promise<Buffer>((resolve, reject) => {
|
const pcapBuffer = await new Promise<Buffer>((resolve, reject) => {
|
||||||
@@ -1819,16 +1829,26 @@ router.post('/:id/tools/capture', requireWrite, async (req: Request, res: Respon
|
|||||||
ssh.sftp((err, sftp) => {
|
ssh.sftp((err, sftp) => {
|
||||||
if (err) { ssh.end(); return reject(new Error(`SFTP init failed: ${err.message}`)); }
|
if (err) { ssh.end(); return reject(new Error(`SFTP init failed: ${err.message}`)); }
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
const stream = sftp.createReadStream(remotePath);
|
const stream = sftp.createReadStream(sftpPath);
|
||||||
stream.on('data', (chunk: Buffer) => chunks.push(chunk));
|
stream.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
stream.on('end', () => {
|
stream.on('end', () => {
|
||||||
// Best-effort delete; don't block the response on it
|
sftp.unlink(sftpPath, () => { ssh.end(); });
|
||||||
sftp.unlink(remotePath, () => { ssh.end(); });
|
|
||||||
resolve(Buffer.concat(chunks));
|
resolve(Buffer.concat(chunks));
|
||||||
});
|
});
|
||||||
stream.on('error', (e: Error) => {
|
stream.on('error', (e: Error) => {
|
||||||
ssh.end();
|
// Try without leading slash in case RouterOS SFTP is rooted differently
|
||||||
reject(new Error(`SFTP read failed (path=${remotePath}): ${e.message}`));
|
if (sftpPath.startsWith('/') && e.message.includes('No such file')) {
|
||||||
|
const altPath = sftpPath.slice(1);
|
||||||
|
console.log(`[capture] retrying SFTP with path: ${altPath}`);
|
||||||
|
const chunks2: Buffer[] = [];
|
||||||
|
const stream2 = sftp.createReadStream(altPath);
|
||||||
|
stream2.on('data', (c: Buffer) => chunks2.push(c));
|
||||||
|
stream2.on('end', () => { sftp.unlink(altPath, () => { ssh.end(); }); resolve(Buffer.concat(chunks2)); });
|
||||||
|
stream2.on('error', (e2: Error) => { ssh.end(); reject(new Error(`SFTP read failed (tried ${sftpPath} and ${altPath}): ${e2.message}`)); });
|
||||||
|
} else {
|
||||||
|
ssh.end();
|
||||||
|
reject(new Error(`SFTP read failed (path=${sftpPath}): ${e.message}`));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user