282 lines
9.1 KiB
TypeScript
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();
|
|
});
|
|
}
|
|
}
|