187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
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<void> {
|
|
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<boolean> {
|
|
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);
|
|
}
|
|
} |