Files
AfiS-Aktensystem/web/lib/files/clamav.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

94 lines
2.9 KiB
TypeScript

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<ScanResult> {
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<boolean> {
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))
})
}