haraka-plugin-avg
Version:
Haraka plugin for the AVG virus scanner
162 lines (142 loc) • 4.79 kB
JavaScript
// avg - AVG virus scanner
const fs = require('node:fs')
const net = require('node:net')
const path = require('node:path')
const net_utils = require('haraka-net-utils')
const smtp_regexp = /^(\d{3})([ -])(.*)/
exports.register = function () {
this.load_avg_ini()
}
exports.load_avg_ini = function () {
this.cfg = this.config.get(
'avg.ini',
{
booleans: ['+defer.timeout', '+defer.error'],
},
() => {
this.load_avg_ini()
},
)
}
exports.get_tmp_file = function (transaction) {
const tmpdir = this.cfg.main.tmpdir || '/tmp'
return path.join(tmpdir, `${transaction.uuid}.tmp`)
}
exports.hook_data_post = function (next, connection) {
if (!connection?.transaction) return next()
const plugin = this
const tmpfile = plugin.get_tmp_file(connection.transaction)
const ws = fs.createWriteStream(tmpfile)
ws.once('error', (err) => {
connection.results.add(plugin, {
err: `Error writing temporary file: ${err.message}`,
})
if (!plugin.cfg.defer.error) return next()
return next(DENYSOFT, 'Virus scanner error (AVG)')
})
ws.once('close', () => {
const start_time = Date.now()
const socket = new net.Socket()
net_utils.add_line_processor(socket)
socket.setTimeout((plugin.cfg.main.connect_timeout || 10) * 1000)
let connected = false
let command = 'connect'
let response = []
function do_next(code, msg) {
fs.unlink(tmpfile, () => {})
return next(code, msg)
}
socket.send_command = function (cmd, data) {
const line = cmd + (data ? ` ${data}` : '')
connection.logprotocol(plugin, `> ${line}`)
this.write(`${line}\r\n`)
command = cmd.toLowerCase()
response = []
}
socket.on('timeout', () => {
const msg = `${connected ? 'connection' : 'session'} timed out`
connection.results.add(plugin, { err: msg })
if (!plugin.cfg.defer.timeout) return do_next()
return do_next(DENYSOFT, 'Virus scanner timeout (AVG)')
})
socket.on('error', (err) => {
connection.results.add(plugin, { err: err.message })
if (!plugin.cfg.defer.error) return do_next()
return do_next(DENYSOFT, 'Virus scanner error (AVG)')
})
socket.on('connect', function () {
connected = true
this.setTimeout((plugin.cfg.main.session_timeout || 30) * 1000)
})
socket.on('line', (line) => {
const matches = smtp_regexp.exec(line)
connection.logprotocol(plugin, `< ${line}`)
if (!matches) {
connection.results.add(plugin, {
err: `Unrecognized response: ${line}`,
})
socket.end()
if (!plugin.cfg.defer.error) return do_next()
return do_next(DENYSOFT, 'Virus scanner error (AVG)')
}
const code = matches[1]
const cont = matches[2]
const rest = matches[3]
response.push(rest)
if (cont !== ' ') return
switch (command) {
case 'connect':
if (code !== '220') {
// Error
connection.results.add(plugin, {
err: `Unrecognized response: ${line}`,
})
if (!plugin.cfg.defer.timeout) return do_next()
return do_next(DENYSOFT, 'Virus scanner error (AVG)')
} else {
socket.send_command('SCAN', tmpfile)
}
break
case 'scan': {
const elapsed = Date.now() - start_time
connection.loginfo(plugin, {
time: `${elapsed}ms`,
code,
response: `"${response.join(' ')}"`,
})
// Check code
switch (code) {
case '200': // 200 ok
// Message did not contain a virus
connection.results.add(plugin, { pass: 'clean' })
socket.send_command('QUIT')
return do_next()
case '403':
// File 'eicar.com', 'Virus identified EICAR_Test'
connection.results.add(plugin, {
fail: response.join(' '),
})
socket.send_command('QUIT')
return do_next(DENY, response.join(' '))
default:
// Any other result is an error
connection.results.add(plugin, {
err: `Bad response: ${response.join(' ')}`,
})
}
socket.send_command('QUIT')
if (!plugin.cfg.defer.error) return do_next()
return do_next(DENYSOFT, 'Virus scanner error (AVG)')
}
case 'quit':
socket.end()
break
default:
throw new Error(`Unknown command: ${command}`)
}
})
socket.connect(plugin.cfg.main.port || 54322, plugin.cfg.main.host)
})
connection.transaction.message_stream.pipe(ws)
}