diff --git a/API.md b/API.md index 52ef073..940a7da 100644 --- a/API.md +++ b/API.md @@ -2,7 +2,7 @@
NodeClam
-

NodeClam class definition. To cf

+

NodeClam class definition.

NodeClamError

Clamscan-specific extension of the Javascript Error object

@@ -14,6 +14,15 @@ chunks into the correct format for a ClamAV socket.

+## Members + +
+
pingPromise.<object>
+

Quick check to see if the remote/local socket is working. Callback/Resolve +response is an instance to a ClamAV socket client.

+
+
+ ## Functions
@@ -26,7 +35,7 @@ By default, it will retrieve files recursively.

## NodeClam -NodeClam class definition. To cf +NodeClam class definition. **Kind**: global class @@ -55,6 +64,7 @@ Initialization method. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - An initated instance of NodeClam +**Access**: public | Param | Type | Default | Description | | --- | --- | --- | --- | @@ -125,6 +135,7 @@ Allows one to create a new instances of clamscan with new options. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - A reset instance of NodeClam +**Access**: public | Param | Type | Description | | --- | --- | --- | @@ -138,6 +149,7 @@ Establish the clamav version of a local or remote clamav daemon. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<string> - - The version of ClamAV that is being interfaced with +**Access**: public | Param | Type | Description | | --- | --- | --- | @@ -165,6 +177,7 @@ be the most common use-case for this module. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - Object like: `{ file: String, isInfected: Boolean, viruses: Array }` +**Access**: public | Param | Type | Description | | --- | --- | --- | @@ -208,6 +221,7 @@ destination (ex. delete a file or S3 object). **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Transform - A Transform stream for piping a Readable stream into +**Access**: public **Example** ```js const NodeClam = require('clamscan'); @@ -257,6 +271,7 @@ Just an alias to `isInfected`. See docs for that for usage examples. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - Object like: `{ file: String, isInfected: Boolean, viruses: Array }` +**Access**: public | Param | Type | Description | | --- | --- | --- | @@ -276,6 +291,7 @@ of scanning many files or directories. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - Object like: `{ goodFiles: Array, badFiles: Array, errors: Object, viruses: Array }` +**Access**: public | Param | Type | Default | Description | | --- | --- | --- | --- | @@ -333,6 +349,7 @@ method also allows for non-recursive scanning with the clamdscan binary. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - Object like: `{ path: String, isInfected: Boolean, goodFiles: Array, badFiles: Array, viruses: Array }` +**Access**: public | Param | Type | Default | Description | | --- | --- | --- | --- | @@ -368,6 +385,7 @@ have access to a local ClamAV binary. **Kind**: instance method of [NodeClam](#NodeClam) **Returns**: Promise.<object> - Object like: `{ file: String, isInfected: Boolean, viruses: Array } ` +**Access**: public | Param | Type | Description | | --- | --- | --- | @@ -470,6 +488,20 @@ This will flush out the stream when all data has been received. | --- | --- | --- | | cb | function | What to do when done | + + +## ping ⇒ Promise.<object> +Quick check to see if the remote/local socket is working. Callback/Resolve +response is an instance to a ClamAV socket client. + +**Kind**: global variable +**Returns**: Promise.<object> - A copy of the Socket/TCP client +**Access**: public + +| Param | Type | Description | +| --- | --- | --- | +| [cb] | function | What to do after the ping | + ## getFiles(dir, [recursive]) ⇒ Array diff --git a/README.md b/README.md index 4d63011..9c33475 100755 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ If you are migrating from v0.8.5 or less to v1.0.0 or greater, please read the [ - [scanFiles](#scanFiles) - [scanStream](#scanStream) - [passthrough](#passthrough) + - [ping](#ping) - [Contribute](#contribute) - [Resources used to help develop this module](#resources-used-to-help-develop-this-module) @@ -652,6 +653,66 @@ output.on('finish', () => { // NOTE: no errors (or other events) are being handled in this example but standard errors will be emitted according to NodeJS's Stream specifications ``` + + +## .ping() + +This method checks to see if the remote/local socket is working. It supports a callback and Promise API. If no callback is supplied, a Promise will be returned. This method can be used for healthcheck purposes and is already implicitly used during scan. + +### Parameters + +- `callback` (function) (optional) Will be called after the ping: + + - `err` (object or null) A standard JavaScript Error object (null if no error) + - `client` (object) A copy of the Socket/TCP client + +### Returns + +- Promise + + - Promise resolution returns: `client` (object): A copy of the Socket/TCP client + +### Examples + +**Callback Example:** + +```javascript +const NodeClam = require('clamscan'); + +// You'll need to specify your socket or TCP connection info +const clamscan = new NodeClam().init({ + clamdscan: { + socket: '/var/run/clamd.scan/clamd.sock', + host: '127.0.0.1', + port: 3310, + } +}); + +clamscan.ping((err, client) => { + if (err) return console.error(err); + console.log('ClamAV is still working!'); + client.end(); +}); +``` + +**Promise Example:** + +```javascript +clamscan.ping().then((client) => { + console.log('ClamAV is still working!'); + client.end(); +}).catch(err => { + console.error(err); +}; +``` + +**Promise Example:** + +```javascript +const client = await clamscan.ping(); +client.end(); +``` + # Contribute Got a missing feature you'd like to use? Found a bug? Go ahead and fork this repo, build the feature and issue a pull request. diff --git a/index.js b/index.js index bdbe0f4..0e4c618 100755 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ /* eslint-disable no-async-promise-executor */ /*! * Node - Clam - * Copyright(c) 2013-2020 Kyle Farris + * Copyright(c) 2013-2024 Kyle Farris * MIT Licensed */ @@ -31,7 +31,7 @@ const fsStat = fsPromises.stat; const cpExecFile = promisify(execFile); /** - * NodeClam class definition. To cf + * NodeClam class definition. * * @typicalname NodeClam */ @@ -39,6 +39,8 @@ class NodeClam { /** * This sets up all the defaults of the instance but does not * necessarily return an initialized instance. Use `.init` for that. + * + * @constructor */ constructor() { this.initialized = false; @@ -82,6 +84,7 @@ class NodeClam { /** * Initialization method. * + * @public * @param {object} [options] - User options for the Clamscan module * @param {boolean} [options.removeInfected=false] - If true, removes infected files when found * @param {boolean|string} [options.quarantineInfected=false] - If not false, should be a string to a path to quarantine infected files @@ -326,7 +329,7 @@ class NodeClam { if (this.settings.debugMode) console.log(`${this.debugLabel}: Initially testing socket/tcp connection to clamscan server.`); try { - const client = await this._ping(); + const client = await this.ping(); client.end(); if (this.settings.debugMode) console.log(`${this.debugLabel}: Established connection to clamscan server!`); @@ -353,6 +356,7 @@ class NodeClam { /** * Allows one to create a new instances of clamscan with new options. * + * @public * @param {object} [options] - Same options as the `init` method * @param {Function} [cb] - What to do after reset (repsponds with reset instance of NodeClam) * @returns {Promise} A reset instance of NodeClam @@ -681,69 +685,13 @@ class NodeClam { } /** - * Quick check to see if the remote/local socket is working. Callback/Resolve - * response is an instance to a ClamAV socket client. - * + * Alias `ping()` for backwards-compatibility with older package versions. + * * @private - * @param {Function} [cb] - What to do after the ping - * @returns {Promise} A copy of the Socket/TCP client + * @alias ping */ _ping(cb) { - let hasCb = false; - - // Verify second param, if supplied, is a function - if (cb && typeof cb !== 'function') - throw new NodeClamError('Invalid cb provided to ping. Second parameter must be a function!'); - - // Making things simpler - if (cb && typeof cb === 'function') hasCb = true; - - // Setup the socket client variable - let client; - - // eslint-disable-next-line consistent-return - return new Promise(async (resolve, reject) => { - try { - client = await this._initSocket('_ping'); - - if (this.settings.debugMode) - console.log(`${this.debugLabel}: Established connection to clamscan server!`); - - client.write('PING'); - - let dataReceived = false; - client.on('end', () => { - if (!dataReceived) { - const err = new NodeClamError('Did not get a PONG response from clamscan server.'); - if (hasCb) cb(err, null); - else reject(err); - } - }); - - client.on('data', (data) => { - if (data.toString().trim() === 'PONG') { - dataReceived = true; - if (this.settings.debugMode) console.log(`${this.debugLabel}: PONG!`); - return hasCb ? cb(null, client) : resolve(client); - } - - // I'm not even sure this case is possible, but... - const err = new NodeClamError( - data, - 'Could not establish connection to the remote clamscan server.' - ); - return hasCb ? cb(err, null) : reject(err); - }); - client.on('error', (err) => { - if (this.settings.debugMode) { - console.log(`${this.debugLabel}: Could not connect to the clamscan server.`, err); - } - return hasCb ? cb(err, null) : reject(err); - }); - } catch (err) { - return hasCb ? cb(err, false) : reject(err); - } - }); + return this.ping(cb); } /** @@ -830,6 +778,7 @@ class NodeClam { /** * Establish the clamav version of a local or remote clamav daemon. * + * @public * @param {Function} [cb] - What to do when version is established * @returns {Promise} - The version of ClamAV that is being interfaced with * @example @@ -927,6 +876,7 @@ class NodeClam { * If no callback is supplied, a Promise will be returned. This method will likely * be the most common use-case for this module. * + * @public * @param {string} file - Path to the file to check * @param {Function} [cb] - What to do after the scan * @returns {Promise} Object like: `{ file: String, isInfected: Boolean, viruses: Array }` @@ -1162,6 +1112,7 @@ class NodeClam { * so that you can decide if there's anything you need to do with the final output * destination (ex. delete a file or S3 object). * + * @public * @returns {Transform} A Transform stream for piping a Readable stream into * @example * const NodeClam = require('clamscan'); @@ -1451,9 +1402,77 @@ class NodeClam { }); } + /** + * Quick check to see if the remote/local socket is working. Callback/Resolve + * response is an instance to a ClamAV socket client. + * + * @public + * @name ping + * @param {Function} [cb] - What to do after the ping + * @returns {Promise} A copy of the Socket/TCP client + */ + ping(cb) { + let hasCb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to ping. Second parameter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') hasCb = true; + + // Setup the socket client variable + let client; + + // eslint-disable-next-line consistent-return + return new Promise(async (resolve, reject) => { + try { + client = await this._initSocket('ping'); + + if (this.settings.debugMode) + console.log(`${this.debugLabel}: Established connection to clamscan server!`); + + client.write('PING'); + + let dataReceived = false; + client.on('end', () => { + if (!dataReceived) { + const err = new NodeClamError('Did not get a PONG response from clamscan server.'); + if (hasCb) cb(err, null); + else reject(err); + } + }); + + client.on('data', (data) => { + if (data.toString().trim() === 'PONG') { + dataReceived = true; + if (this.settings.debugMode) console.log(`${this.debugLabel}: PONG!`); + return hasCb ? cb(null, client) : resolve(client); + } + + // I'm not even sure this case is possible, but... + const err = new NodeClamError( + data, + 'Could not establish connection to the remote clamscan server.' + ); + return hasCb ? cb(err, null) : reject(err); + }); + client.on('error', (err) => { + if (this.settings.debugMode) { + console.log(`${this.debugLabel}: Could not connect to the clamscan server.`, err); + } + return hasCb ? cb(err, null) : reject(err); + }); + } catch (err) { + return hasCb ? cb(err, false) : reject(err); + } + }); + } + /** * Just an alias to `isInfected`. See docs for that for usage examples. * + * @public * @param {string} file - Path to the file to check * @param {Function} [cb] - What to do after the scan * @returns {Promise} Object like: `{ file: String, isInfected: Boolean, viruses: Array }` @@ -1471,6 +1490,7 @@ class NodeClam { * * **NOTE:** The only way to get per-file notifications is through the callback API. * + * @public * @param {Array} files - A list of files or paths (full paths) to be scanned * @param {Function} [endCb] - What to do after the scan completes * @param {Function} [fileCb] - What to do after each file has been scanned @@ -1901,6 +1921,7 @@ class NodeClam { * using the `clamscan` binary. Doing so with `clamdscan` is okay, however. This * method also allows for non-recursive scanning with the clamdscan binary. * + * @public * @param {string} path - The directory to scan files of * @param {Function} [endCb] - What to do when all files have been scanned * @param {Function} [fileCb] - What to do after each file has been scanned @@ -2166,6 +2187,7 @@ class NodeClam { * use of a TCP or UNIX Domain socket. In other words, this will not work if you only * have access to a local ClamAV binary. * + * @public * @param {Readable} stream - A readable stream to scan * @param {Function} [cb] - What to do when the socket response with results * @returns {Promise} Object like: `{ file: String, isInfected: Boolean, viruses: Array } ` diff --git a/tests/index.js b/tests/index.js index c56bedb..1648064 100755 --- a/tests/index.js +++ b/tests/index.js @@ -410,6 +410,49 @@ describe('_initSocket', () => { }); }); +describe('ping', () => { + let clamscan; + beforeEach(async () => { + clamscan = await resetClam(); + }); + + it('should exist', () => { + should.exist(clamscan.ping); + }); + it('should be a function', () => { + clamscan.ping.should.be.a('function'); + }); + + it('should respond with a socket client (Promise API)', async () => { + const client = await clamscan.ping(); + expect(client).to.be.an('object'); + expect(client.readyState).to.eql('open'); + expect(client.writable).to.eql(true); + expect(client.readable).to.eql(true); + expect(client._hadError).to.eql(false); + expect(client).to.respondTo('on'); + expect(client).to.respondTo('end'); + expect(client).to.not.respondTo('foobar'); + + client.end(); + }); + + it('should respond with a socket client (Callback API)', (done) => { + clamscan.ping((err, client) => { + check(done, () => { + expect(err).to.not.be.instanceof(Error); + expect(client).to.be.an('object'); + expect(client.writable).to.eql(true); + expect(client.readable).to.eql(true); + expect(client._hadError).to.eql(false); + expect(client).to.respondTo('on'); + expect(client).to.respondTo('end'); + expect(client).to.not.respondTo('foobar'); + }); + }); + }); +}); + describe('_ping', () => { let clamscan; beforeEach(async () => { @@ -1744,7 +1787,7 @@ if (process.env.CI) { tls: true, }, }); - (await clamscan._ping()).end(); + (await clamscan.ping()).end(); }); it('Connects to clamd server via a TLS proxy on 127.0.0.1', async () => { @@ -1756,7 +1799,7 @@ if (process.env.CI) { tls: true, }, }); - (await clamscan._ping()).end(); + (await clamscan.ping()).end(); }); it('Connects to clamd server via a TLS proxy on ::1', async () => { @@ -1768,7 +1811,7 @@ if (process.env.CI) { tls: true, }, }); - (await clamscan._ping()).end(); + (await clamscan.ping()).end(); }); // it('Connects to clamd server via a TLS proxy on implicit localhost', async () => {