clamdjs
Version:
A ClamAV client on node.js
385 lines (344 loc) • 10.4 kB
JavaScript
/**
* Module dependencies.
*/
const net = require('net')
const fs = require('fs')
const path = require('path')
const Readable = require('stream').Readable
const Transform = require('stream').Transform
/**
* Module exports.
*/
module.exports = {
createScanner,
ping,
version,
isCleanReply
}
/**
* Create a scanner
*
* @param {string} host clamav server's host
* @param {number} port clamav sever's port
* @return {object}
* @public
*/
function createScanner (host, port) {
if (!host || !port) throw new Error('must provide the host and port that clamav server listen to')
/**
* scan a read stream
* @param {object} readStream
* @param {number} [timeout = 5000] the socket's timeout option
* @return {Promise}
*/
function scanStream (readStream, timeout) {
if (typeof timeout !== 'number' || timeout < 0) timeout = 5000
return new Promise(function (resolve, reject) {
let readFinished = false
const socket = net.createConnection({
host,
port
}, function () {
socket.write('zINSTREAM\0')
// fotmat the chunk
readStream.pipe(chunkTransform()).pipe(socket)
readStream
.on('end', function () {
readFinished = true
readStream.destroy()
})
.on('error', reject)
})
let replys = []
socket.setTimeout(timeout)
socket
.on('data', function (chunk) {
clearTimeout(connectAttemptTimer)
if (!readStream.isPaused()) readStream.pause()
replys.push(chunk)
})
.on('end', function () {
clearTimeout(connectAttemptTimer)
let reply = Buffer.concat(replys)
if (!readFinished) reject(new Error('Scan aborted. Reply from server: ' + reply))
else resolve(reply.toString())
})
.on('error', reject)
const connectAttemptTimer = setTimeout(function () {
socket.destroy(new Error('Timeout connecting to server'));
}, timeout)
})
}
/**
* scan a Buffer
* @param {string} path
* @param {number} [timeout = 5000] the socket's timeout option
* @param {number} [chunkSize = 64kb] size of the chunk, which send to Clamav server
* @return {Promise}
*/
function scanBuffer (buffer, timeout, chunkSize) {
if (typeof timeout !== 'number' || timeout < 0) timeout = 5000
if (typeof chunkSize !== 'number') chunkSize = 64 * 1024
let start = 0
const bufReader = new Readable({
highWaterMark: chunkSize,
read (size) {
if (start < buffer.length) {
let block = buffer.slice(start, start + size)
this.push(block)
start += block.length
} else {
this.push(null)
}
}
})
return scanStream(bufReader, timeout)
}
/**
* scan a file
* @param {string} filePath
* @param {number} [timeout = 5000] the socket's timeout option
* @param {number} [chunkSize = 64kb] size of the chunk, which send to Clamav server
* @return {Promise}
*/
function scanFile (filePath, timeout, chunkSize) {
filePath = path.normalize(filePath)
if (typeof timeout !== 'number' || timeout < 0) timeout = 5000
if (typeof chunkSize !== 'number') chunkSize = 64 * 1024
try {
let stats = fs.statSync(filePath)
if (stats.isDirectory()) {
return Promise.reject(new Error(filePath + ' is a directory, please use scanDirectory instead!'))
} else if (!stats.isFile() || stats.isSymbolicLink()){
return Promise.reject(new Error(filePath + ' is Not a regular file'))
}
} catch (error) {
return Promise.reject(error)
}
let s = fs.createReadStream(filePath, {
highWaterMark: chunkSize
})
return scanStream(s, timeout)
}
/**
* scan a directory
* @param {string} rootPath
* @param {object} [options]
* @return {Promise}
*/
function scanDirectory (rootPath, options) {
// TODO add ignore option
rootPath = path.normalize(rootPath)
let opts = options || {}
let timeout = typeof opts.timeout !== 'number'
? 5000
: opts.timeout
let chunkSize = opts.chunkSize || 64 * 1024
let scanningFile = opts.scanningFile || 10
let detail = opts.detail !== false
let cont = opts.cont !== false
return new Promise(function (resolve, reject) {
// scanning result
let ScannedFiles = 0
let Infected = 0
let EncounterError = 0
let Result = []
// scaning queue's state
let scanning = 0
// keep track of the files and directories path, which upcoming to scan
let dirs = []
let files = []
function scanDir (pathName) {
let flist = null
try {
flist = fs.readdirSync(pathName)
} catch (error) {
if (cont) return done(pathName, null, error.message, 0)
else return reject(error)
}
flist.forEach(function (entry) {
let stats
try {
stats = fs.lstatSync(path.join(pathName, entry))
} catch (error) {
if (cont) return done(pathName, null, error.message, 0)
else return reject(error)
}
if (stats.isDirectory() && !stats.isSymbolicLink()) {
dirs.push(path.join(pathName, entry))
} else if (stats.isFile() && !stats.isSymbolicLink()) {
files.push(path.join(pathName, entry))
}
})
// schedul scaning queue after scanned a directory
schedulScan()
}
function scanFileWrap (path) {
scanning = scanning + 1
scanFile(path, timeout, chunkSize)
.then(function (res) {
done(path, res, null, 1)
})
.catch(function (e) {
done(path, null, e.message, 1)
})
}
function done (file, reply, errorMsg, finished) {
scanning = scanning - finished
ScannedFiles = ScannedFiles + 1
if (reply !== null && !isCleanReply(reply))Infected = Infected + 1
if (errorMsg !== null)EncounterError = EncounterError + 1
if (detail) {
Result.push({
file,
reply,
errorMsg
})
} else if (errorMsg !== null || (reply !== null && !isCleanReply(reply))) {
Result.push({
file,
reply,
errorMsg
})
}
if (files.length !== 0) scanFileWrap(files.shift())
else if (dirs.length !== 0) scanDir(dirs.shift())
else if (scanning === 0) {
resolve({
ScannedFiles,
Infected,
EncounterError,
Result
})
}
}
function schedulScan () {
if (scanning >= scanningFile) return
if (files.length !== 0) {
while (scanning < scanningFile && files.length !== 0) {
scanFileWrap(files.shift())
}
} else if (dirs.length !== 0) scanDir(dirs.shift())
else if (scanning === 0) {
resolve({
ScannedFiles,
Infected,
EncounterError,
Result
})
}
}
let stats = null
try {
stats = fs.lstatSync(rootPath)
} catch (error) {
if (cont) return done(rootPath, null, error.message, 0)
else return reject(error)
}
if (stats.isDirectory() && !stats.isSymbolicLink()) scanDir(rootPath)
else if (stats.isFile() && !stats.isSymbolicLink()) scanFileWrap(rootPath)
else reject(new Error(rootPath + ' is Not a regular file or directory'))
})
}
return {
scanStream,
scanBuffer,
scanFile,
scanDirectory
}
}
/**
* Check the daemon’s state
*
* @param {string} host clamav server's host
* @param {number} port clamav sever's port
* @param {number} [timeout = 5000] the socket's timeout option
* @return {boolean}
* @public
*/
function ping (host, port, timeout) {
if (!host || !port) throw new Error('must provide the host and port that clamav server listen to')
if (typeof timeout !== 'number' || timeout < 0) timeout = 5000
return _command(host, port, timeout, 'zPING\0')
.then(function (res) {
return res.equals(Buffer.from('PONG\0'))
})
}
/**
* Get clamav version detail.
*
* @param {string} host clamav server's host
* @param {number} port clamav sever's port
* @param {number} [timeout = 5000] pass to sets the socket's timeout optine
* @return {string}
* @public
*/
function version (host, port, timeout) {
if (!host || !port) throw new Error('must provide the host and port that clamav server listen to')
if (typeof timeout !== 'number' || timeout < 0) timeout = 5000
return _command(host, port, timeout, 'zVERSION\0')
.then(function (res) {
return res.toString()
})
}
/**
* Check the reply mean the file infect or not
*
* @param {*} reply get from the scanner
* @return {boolean}
* @public
*/
function isCleanReply (reply) {
return reply.includes('OK') && !reply.includes('FOUND')
}
/**
* transform the chunk from read stream to the fotmat that clamav server expect
*
* @return {object} stream.Transform
*/
function chunkTransform () {
return new Transform(
{
transform (chunk, encoding, callback) {
const length = Buffer.alloc(4)
length.writeUInt32BE(chunk.length, 0)
this.push(length)
this.push(chunk)
callback()
},
flush (callback) {
const zore = Buffer.alloc(4)
zore.writeUInt32BE(0, 0)
this.push(zore)
callback()
}
})
}
/**
* helper function for single command function like ping() and version()
* @param {string} host
* @param {number} port
* @param {number} timeout
* @param {string} command will send to clamav server, either 'zPING\0' or 'zVERSION\0'
*/
function _command (host, port, timeout, command) {
return new Promise(function (resolve, reject) {
const client = net.createConnection({
host,
port
}, function () {
client.write(command)
})
client.setTimeout(timeout)
let replys = []
client
.on('data', function (chunk) {
replys.push(chunk)
})
.on('end', function () {
resolve(Buffer.concat(replys))
})
.on('error', reject)
})
}