smartantivirus/ts/classes.clamav.manager.ts

282 lines
9.1 KiB
TypeScript

import { exec, spawn, net, promisify, EventEmitter, execAsync } from './plugins.js';
export interface ClamAVLogEvent {
timestamp: string;
message: string;
type: 'update' | 'scan' | 'system' | 'error';
}
export class ClamAVManager extends EventEmitter {
private containerId: string | null = null;
private containerName = 'clamav-daemon';
private imageTag = 'clamav/clamav:latest';
private port = 3310;
private logs: ClamAVLogEvent[] = [];
constructor() {
super();
}
public getLogs(): ClamAVLogEvent[] {
return this.logs;
}
/**
* Start the ClamAV container if it's not already running
*/
public async startContainer(): Promise<void> {
try {
console.log('[ClamAV] Starting container initialization...');
// Check if container is already running
const { stdout: psOutput } = await execAsync('docker ps --filter name=' + this.containerName);
if (psOutput.includes(this.containerName)) {
console.log('[ClamAV] Container is already running');
this.containerId = (await execAsync(`docker ps -q --filter name=${this.containerName}`)).stdout.trim();
console.log('[ClamAV] Container ID:', this.containerId);
this.attachLogWatcher();
await this.waitForInitialization();
return;
}
// Check if container exists but is stopped
const { stdout: psaOutput } = await execAsync('docker ps -a --filter name=' + this.containerName);
if (psaOutput.includes(this.containerName)) {
console.log('[ClamAV] Found stopped container, starting it...');
await execAsync(`docker start ${this.containerName}`);
this.containerId = (await execAsync(`docker ps -q --filter name=${this.containerName}`)).stdout.trim();
console.log('[ClamAV] Started existing container, ID:', this.containerId);
} else {
// Create and start new container
console.log('[ClamAV] Creating new container...');
const { stdout } = await execAsync(
`docker run -d --name ${this.containerName} -p ${this.port}:3310 ${this.imageTag}`
);
this.containerId = stdout.trim();
console.log('[ClamAV] Created new container, ID:', this.containerId);
}
this.attachLogWatcher();
console.log('[ClamAV] Waiting for initialization...');
await this.waitForInitialization();
console.log('[ClamAV] Container successfully initialized');
} catch (error) {
console.error('[ClamAV] Error starting container:', error);
throw error;
}
}
/**
* Stop the ClamAV container
*/
public async stopContainer(): Promise<void> {
if (!this.containerId) {
console.log('No ClamAV container is running');
return;
}
try {
await execAsync(`docker stop ${this.containerId}`);
console.log('Stopped ClamAV container');
} catch (error) {
console.error('Error stopping ClamAV container:', error);
throw error;
}
}
/**
* Manually trigger a database update
*/
public async updateDatabase(): Promise<void> {
if (!this.containerId) {
throw new Error('ClamAV container is not running');
}
try {
// First check if freshclam is already running
const { stdout: psOutput } = await execAsync(`docker exec ${this.containerId} ps aux | grep freshclam`);
if (psOutput.includes('/usr/local/sbin/freshclam -d')) {
console.log('Freshclam daemon is already running');
// Wait a bit to ensure database is updated
await new Promise(resolve => setTimeout(resolve, 2000));
return;
}
// If not running as daemon, try to update manually
const { stdout, stderr } = await execAsync(`docker exec ${this.containerId} freshclam --no-warnings`);
console.log('Database update output:', stdout);
if (stderr) {
console.error('Database update errors:', stderr);
}
} catch (error) {
// Check if the error is due to freshclam already running
if (error.stderr?.includes('ERROR: Problem with internal logger') ||
error.stdout?.includes('Resource temporarily unavailable')) {
console.log('Freshclam is already running, skipping manual update');
return;
}
console.error('Error updating ClamAV database:', error);
throw error;
}
}
/**
* Get the current database version information
*/
public async getDatabaseInfo(): Promise<string> {
if (!this.containerId) {
throw new Error('ClamAV container is not running');
}
try {
// Try both .cld and .cvd files since ClamAV can use either format
try {
const { stdout } = await execAsync(`docker exec ${this.containerId} sigtool --info /var/lib/clamav/daily.cld`);
return stdout;
} catch {
const { stdout } = await execAsync(`docker exec ${this.containerId} sigtool --info /var/lib/clamav/daily.cvd`);
return stdout;
}
} catch (error) {
console.error('Error getting database info:', error);
throw error;
}
}
/**
* Watch container logs and emit events for different types of log messages
*/
private attachLogWatcher(): void {
if (!this.containerId) return;
const logProcess = spawn('docker', ['logs', '-f', this.containerId]);
logProcess.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
lines.forEach(line => {
if (!line.trim()) return;
const event: ClamAVLogEvent = {
timestamp: new Date().toISOString(),
message: line,
type: this.determineLogType(line)
};
this.logs.push(event);
this.emit('log', event);
console.log(`[ClamAV ${event.type}] ${event.message}`);
});
});
logProcess.stderr.on('data', (data) => {
const event: ClamAVLogEvent = {
timestamp: new Date().toISOString(),
message: data.toString(),
type: 'error'
};
this.logs.push(event);
this.emit('log', event);
console.error(`[ClamAV error] ${event.message}`);
});
logProcess.on('error', (error) => {
console.error('Error in log watcher:', error);
});
}
/**
* Determine the type of log message
*/
private determineLogType(logMessage: string): ClamAVLogEvent['type'] {
const lowerMessage = logMessage.toLowerCase();
if (lowerMessage.includes('update') || lowerMessage.includes('freshclam')) {
return 'update';
} else if (lowerMessage.includes('scan') || lowerMessage.includes('found')) {
return 'scan';
} else if (lowerMessage.includes('error') || lowerMessage.includes('warning')) {
return 'error';
}
return 'system';
}
/**
* Wait for ClamAV to initialize by checking both logs and service readiness
*/
private async waitForInitialization(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.containerId) {
reject(new Error('Container ID not set'));
return;
}
let timeout: NodeJS.Timeout;
let checkCount = 0;
const maxChecks = 60; // Check for 60 seconds
const startTime = Date.now();
// Check service readiness
const checkService = async () => {
try {
const elapsedTime = Math.round((Date.now() - startTime) / 1000);
console.log(`[ClamAV] Checking service readiness (attempt ${checkCount + 1}, ${elapsedTime}s elapsed)...`);
// First check if the service is accepting connections
const client = new net.Socket();
await new Promise<void>((resolveConn, rejectConn) => {
const connectTimeout = setTimeout(() => {
client.destroy();
rejectConn(new Error('Connection timeout'));
}, 1000);
client.connect(this.port, 'localhost', () => {
clearTimeout(connectTimeout);
client.end();
resolveConn();
});
client.on('error', (err) => {
clearTimeout(connectTimeout);
rejectConn(err);
});
});
// Verify the service is responding to commands
const { stdout } = await execAsync(`echo PING | nc localhost ${this.port}`);
if (!stdout.includes('PONG')) {
throw new Error('Service not responding to commands');
}
// If we can connect and get a PONG, the service is ready
console.log('[ClamAV] Service is accepting connections and responding to commands');
cleanup();
resolve();
} catch (error) {
// Service not ready yet, will retry
if (checkCount >= maxChecks) {
cleanup();
reject(new Error(`ClamAV initialization timed out after ${maxChecks} seconds. Last error: ${error.message}`));
return;
}
checkCount++;
}
};
const cleanup = () => {
clearTimeout(timeout);
clearInterval(serviceCheck);
};
const serviceCheck = setInterval(checkService, 1000);
timeout = setTimeout(() => {
cleanup();
reject(new Error('ClamAV initialization timed out after 60 seconds'));
}, 60000);
// Start initial service check
checkService();
});
}
}