import net from 'node:net' /** * Anbindung an clamd über den Unix-Socket im geteilten Volume. * * Es wird **INSTREAM** benutzt, nicht SCAN mit einem Pfad: dann muss der * Scanner die Datei nicht sehen können, und die Anbindung funktioniert auch, * wenn clamd anderswo läuft. Das Protokoll ist simpel — Länge als 4 Byte, * Nutzdaten, dann eine Null-Länge als Abschluss. */ export type ScanResult = | { ok: true; clean: true } | { ok: true; clean: false; signature: string } /** Scanner nicht erreichbar, Zeitüberschreitung, unverständliche Antwort. */ | { ok: false; reason: string } const CHUNK = 64 * 1024 export function scanBuffer( socketPath: string, content: Uint8Array, timeoutMs = 30_000, ): Promise { return new Promise((resolve) => { let settled = false let antwort = '' const socket = net.createConnection(socketPath) const done = (result: ScanResult) => { if (settled) return settled = true socket.destroy() resolve(result) } socket.setTimeout(timeoutMs) socket.on('timeout', () => done({ ok: false, reason: 'Zeitüberschreitung beim Scannen' })) socket.on('error', (error) => done({ ok: false, reason: `clamd nicht erreichbar: ${error.message}` }), ) socket.on('connect', () => { socket.write('zINSTREAM\0') for (let offset = 0; offset < content.length; offset += CHUNK) { const teil = content.subarray(offset, offset + CHUNK) const laenge = Buffer.alloc(4) laenge.writeUInt32BE(teil.length, 0) socket.write(laenge) socket.write(teil) } socket.write(Buffer.from([0, 0, 0, 0])) }) socket.on('data', (chunk) => { antwort += chunk.toString('utf8') }) socket.on('close', () => { const text = antwort.replace(/\0/g, '').trim() if (text.endsWith('OK')) return done({ ok: true, clean: true }) if (text.endsWith('FOUND')) { const signature = text.replace(/^stream:\s*/, '').replace(/\s*FOUND$/, '') return done({ ok: true, clean: false, signature }) } // "size limit exceeded", "ERROR" oder gar nichts. done({ ok: false, reason: text || 'keine Antwort von clamd' }) }) }) } /** * Fragt clamd über den Unix-Socket. Der Socket liegt im geteilten Volume * clamav_socket, siehe compose.yaml. Antwort auf zPING ist "PONG". */ export function pingClamav(socketPath: string, timeoutMs = 2000): Promise { return new Promise((resolve) => { let settled = false const done = (ok: boolean) => { if (settled) return settled = true socket.destroy() resolve(ok) } const socket = net.createConnection(socketPath) socket.setTimeout(timeoutMs) socket.on('connect', () => socket.write('zPING\0')) socket.on('data', (chunk) => done(chunk.toString().includes('PONG'))) socket.on('timeout', () => done(false)) socket.on('error', () => done(false)) }) }