// tshark.js - Secure tshark Execution Wrapper
const { spawn } = require('child_process');
const { promisify } = require('util');
const { exec } = require('child_process');
const which = require('which');
const fs = require('fs').promises;
const crypto = require('crypto');
const path = require('path');
const os = require('os');
const config = require('./config');
const execAsync = promisify(exec);
class TsharkExecutor {
constructor() {
this.tsharkPath = null;
this.initialized = false;
}
/**
* Initialize and locate tshark
*/
async init() {
if (this.initialized) return this.tsharkPath;
console.error('[TSHARK] Locating tshark...');
// Check if path is configured
if (config.tshark.path) {
try {
await this.validateTsharkPath(config.tshark.path);
this.tsharkPath = config.tshark.path;
this.initialized = true;
console.error(`[TSHARK] Using configured path: ${this.tsharkPath}`);
return this.tsharkPath;
} catch (error) {
console.error(`[TSHARK] Configured path invalid: ${error.message}`);
}
}
// Try using 'which' to find tshark in PATH
try {
this.tsharkPath = await which('tshark');
await this.validateTsharkPath(this.tsharkPath);
this.initialized = true;
console.error(`[TSHARK] Found in PATH: ${this.tsharkPath}`);
return this.tsharkPath;
} catch (error) {
console.error(`[TSHARK] Not found in PATH: ${error.message}`);
}
// Try fallback paths
for (const fallbackPath of config.tshark.fallbackPaths) {
try {
await this.validateTsharkPath(fallbackPath);
this.tsharkPath = fallbackPath;
this.initialized = true;
console.error(`[TSHARK] Found at fallback: ${this.tsharkPath}`);
return this.tsharkPath;
} catch (error) {
console.error(`[TSHARK] Fallback ${fallbackPath} failed: ${error.message}`);
}
}
throw new Error(
'tshark not found. Please install Wireshark (https://www.wireshark.org/) and ensure tshark is in your PATH'
);
}
/**
* Validate that a tshark path is executable
*/
async validateTsharkPath(tsharkPath) {
try {
await fs.access(tsharkPath, fs.constants.X_OK);
const { stdout } = await execAsync(`"${tsharkPath}" -v`, { timeout: 5000 });
if (!stdout.includes('TShark')) {
throw new Error('Not a valid tshark executable');
}
} catch (error) {
throw new Error(`Invalid tshark path: ${error.message}`);
}
}
/**
* Securely execute tshark with arguments (NO SHELL)
*/
async execute(args, options = {}) {
await this.init();
const {
timeout = 60000,
maxOutputSize = config.security.maxOutputSize,
onProgress = null
} = options;
return new Promise((resolve, reject) => {
console.error(`[TSHARK] Executing: tshark ${args.join(' ')}`);
// Use spawn without shell to prevent injection
const child = spawn(this.tsharkPath, args, {
shell: false, // CRITICAL: No shell interpretation
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin`
}
});
let stdout = '';
let stderr = '';
let killed = false;
let outputSizeExceeded = false;
// Set timeout
const timeoutHandle = setTimeout(() => {
if (!killed) {
child.kill('SIGTERM');
killed = true;
reject(new Error(`tshark execution timed out after ${timeout}ms`));
}
}, timeout);
// Capture stdout with size limit
child.stdout.on('data', (data) => {
stdout += data.toString();
// Enforce output size limit
if (stdout.length > maxOutputSize) {
if (!killed) {
child.kill('SIGTERM');
killed = true;
outputSizeExceeded = true;
reject(new Error(
`Output size exceeded limit of ${maxOutputSize} bytes`
));
}
}
if (onProgress && !killed) {
onProgress({ stdout: stdout.length, stderr: stderr.length });
}
});
// Capture stderr
child.stderr.on('data', (data) => {
stderr += data.toString();
if (stderr.length > maxOutputSize) {
if (!killed) {
child.kill('SIGTERM');
killed = true;
outputSizeExceeded = true;
reject(new Error(
`Error output size exceeded limit of ${maxOutputSize} bytes`
));
}
}
});
// Handle execution errors
child.on('error', (error) => {
clearTimeout(timeoutHandle);
if (!killed) {
reject(new Error(`tshark execution failed: ${error.message}`));
}
});
// Handle completion
child.on('close', (code) => {
clearTimeout(timeoutHandle);
if (killed || outputSizeExceeded) {
return; // Already rejected
}
if (code !== 0) {
reject(new Error(
`tshark exited with code ${code}${stderr ? ': ' + stderr : ''}`
));
} else {
resolve({ stdout, stderr, exitCode: code });
}
});
});
}
/**
* Create a secure temporary file for captures
*/
createTempFile(prefix = 'wiremcp') {
const random = crypto.randomBytes(16).toString('hex');
const timestamp = Date.now();
const filename = `${prefix}_${timestamp}_${random}.pcap`;
return path.join(config.security.tempDir, filename);
}
/**
* Ensure temp directory exists
*/
async ensureTempDir() {
try {
await fs.mkdir(config.security.tempDir, { recursive: true, mode: 0o700 });
} catch (error) {
console.error(`[TSHARK] Failed to create temp directory: ${error.message}`);
throw error;
}
}
/**
* Capture packets to a file
*/
async capture(interface, duration, outputFile = null) {
await this.ensureTempDir();
const tempFile = outputFile || this.createTempFile('capture');
const timeout = (duration + config.tshark.timeoutBuffer) * 1000;
const maxSize = Math.floor(config.security.maxCaptureSize / 1024); // Convert to KB
try {
await this.execute(
[
'-i', interface,
'-w', tempFile,
'-a', `duration:${duration}`,
'-a', `filesize:${maxSize}`,
'-q' // Quiet mode
],
{ timeout }
);
// Verify file was created and is not empty
const stats = await fs.stat(tempFile);
if (stats.size === 0) {
throw new Error('Capture produced empty file - no packets captured');
}
console.error(`[TSHARK] Captured ${stats.size} bytes to ${tempFile}`);
return tempFile;
} catch (error) {
// Clean up on error
await this.cleanupFile(tempFile);
throw error;
}
}
/**
* Read and parse PCAP file to JSON
*/
async readPcap(pcapFile, fields = [], maxPackets = null) {
const args = ['-r', pcapFile, '-T', 'json'];
// Add fields if specified
fields.forEach(field => {
args.push('-e', field);
});
// Limit packet count if specified
if (maxPackets) {
args.push('-c', maxPackets.toString());
}
const result = await this.execute(args);
try {
return JSON.parse(result.stdout);
} catch (error) {
throw new Error(`Failed to parse tshark JSON output: ${error.message}`);
}
}
/**
* Get statistics from PCAP file
*/
async getStats(pcapFile, statType = 'phs') {
const validStatTypes = ['phs', 'conv,tcp', 'conv,udp', 'io,phs', 'endpoints,tcp'];
if (!validStatTypes.includes(statType)) {
throw new Error(`Invalid stat type: ${statType}`);
}
const result = await this.execute(['-r', pcapFile, '-qz', statType]);
return result.stdout;
}
/**
* Extract fields from PCAP
*/
async extractFields(pcapFile, fields) {
const args = ['-r', pcapFile, '-T', 'fields'];
fields.forEach(field => {
args.push('-e', field);
});
const result = await this.execute(args);
return result.stdout;
}
/**
* Clean up a temporary file
*/
async cleanupFile(filePath) {
try {
await fs.unlink(filePath);
console.error(`[TSHARK] Cleaned up: ${filePath}`);
} catch (error) {
console.error(`[TSHARK] Failed to cleanup ${filePath}: ${error.message}`);
}
}
/**
* Clean up old temporary files
*/
async cleanupOldFiles(maxAgeMs = 3600000) { // Default 1 hour
try {
const files = await fs.readdir(config.security.tempDir);
const now = Date.now();
let cleaned = 0;
for (const file of files) {
if (!file.startsWith('wiremcp_')) continue;
const filePath = path.join(config.security.tempDir, file);
const stats = await fs.stat(filePath);
if (now - stats.mtimeMs > maxAgeMs) {
await this.cleanupFile(filePath);
cleaned++;
}
}
if (cleaned > 0) {
console.error(`[TSHARK] Cleaned up ${cleaned} old temp files`);
}
} catch (error) {
console.error(`[TSHARK] Cleanup failed: ${error.message}`);
}
}
}
// Export singleton instance
module.exports = new TsharkExecutor();