Skip to main content
Glama
tshark.js9.43 kB
// 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();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/anishphilip012git/WireMCP-Secure'

If you have feedback or need assistance with the MCP directory API, please join our Discord server