import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { net } from './plugins.js'; import { ClamAVManager } from './classes.clamav.manager.js'; export class ClamAvService { private host: string; private port: number; private manager: ClamAVManager; constructor(host: string = '127.0.0.1', port: number = 3310) { this.host = host; this.port = port; this.manager = new ClamAVManager(); // Listen to ClamAV logs this.manager.on('log', (event) => { if (event.type === 'scan') { console.log(`[ClamAV Scan] ${event.message}`); } }); } private async ensureContainerStarted(): Promise { await this.manager.startContainer(); } /** * Scans an in-memory Buffer using ClamAV daemon's INSTREAM command. */ public async scanBuffer(buffer: Buffer): Promise<{ isInfected: boolean; reason?: string }> { await this.ensureContainerStarted(); return new Promise((resolve, reject) => { const client = new net.Socket(); client.connect(this.port, this.host, () => { console.log('Connected to ClamAV daemon'); client.write('zINSTREAM\0'); // Start the INSTREAM command const chunkSize = 1024; let offset = 0; // Send data in chunks while (offset < buffer.length) { const chunk = buffer.slice(offset, offset + chunkSize); console.log('Sending chunk:', chunk.toString('utf8')); const sizeBuf = Buffer.alloc(4); sizeBuf.writeUInt32BE(chunk.length, 0); client.write(sizeBuf); client.write(chunk); offset += chunkSize; } // Send end-of-stream signal const endOfStream = Buffer.alloc(4); endOfStream.writeUInt32BE(0, 0); console.log('Sending end-of-stream signal'); client.write(endOfStream); }); client.on('data', (data) => { const response = data.toString(); console.log('Raw Response from ClamAV:', response); const isInfected = response.includes('FOUND'); const reason = isInfected ? response.split('FOUND')[0].trim() : undefined; resolve({ isInfected, reason }); client.end(); }); client.on('error', (err) => { console.error('Error communicating with ClamAV:', err); reject(err); }); client.on('close', () => { console.log('Connection to ClamAV daemon closed'); }); }); } /** * Scans a string by converting it to a Buffer and using scanBuffer. */ public async scanString(input: string): Promise<{ isInfected: boolean; reason?: string }> { console.log('Scanning string:', input); // Debug the input string const buffer = Buffer.from(input, 'utf8'); console.log('Converted buffer:', buffer.toString('utf8')); // Debug the converted buffer return this.scanBuffer(buffer); } /** * Verifies the ClamAV daemon is reachable. */ public async verifyConnection(): Promise { await this.ensureContainerStarted(); return new Promise((resolve, reject) => { const client = new net.Socket(); client.connect(this.port, this.host, () => { console.log('Successfully connected to ClamAV daemon'); client.end(); resolve(true); }); client.on('error', (err) => { console.error('Failed to connect to ClamAV daemon:', err); reject(err); }); }); } /** * Scans data from a NodeJS stream using ClamAV daemon's INSTREAM command. */ public async scanStream(stream: NodeJS.ReadableStream): Promise<{ isInfected: boolean; reason?: string }> { await this.ensureContainerStarted(); return new Promise((resolve, reject) => { const client = new net.Socket(); client.connect(this.port, this.host, () => { console.log('Connected to ClamAV daemon for stream scanning'); client.write('zINSTREAM\0'); stream.on('data', (chunk: Buffer) => { const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); const sizeBuf = Buffer.alloc(4); sizeBuf.writeUInt32BE(buf.length, 0); client.write(sizeBuf); client.write(buf); }); stream.on('end', () => { const endOfStream = Buffer.alloc(4); endOfStream.writeUInt32BE(0, 0); console.log('Stream ended, sending end-of-stream signal'); client.write(endOfStream); }); stream.on('error', (err) => { console.error('Error reading stream:', err); reject(err); }); }); client.on('data', (data) => { const response = data.toString(); console.log('Raw Response from ClamAV (stream):', response); const isInfected = response.includes('FOUND'); const reason = isInfected ? response.split('FOUND')[0].trim() : undefined; resolve({ isInfected, reason }); client.end(); }); client.on('error', (err) => { console.error('Error with ClamAV stream scanning:', err); reject(err); }); }); } /** * Scans a file from a web URL as a stream using ClamAV daemon's INSTREAM command. */ public async scanFileFromWebAsStream(url: string): Promise<{ isInfected: boolean; reason?: string }> { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? plugins.https : plugins.http; protocol.get(url, (response) => { this.scanStream(response).then(resolve).catch(reject); }).on('error', (err) => { console.error('Error fetching URL:', err); reject(err); }); }); } /** * Scans a web resource by URL using ClamAV daemon's INSTREAM command. */ public async scanWebStream(webstreamArg: ReadableStream): Promise<{ isInfected: boolean; reason?: string }> { // Convert the web ReadableStream to a NodeJS ReadableStream const nodeStream = plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webstreamArg); return this.scanStream(nodeStream); } }