Skip to content

Commit

Permalink
Initial structure
Browse files Browse the repository at this point in the history
  • Loading branch information
d-gubert committed Feb 1, 2021
1 parent a8304f7 commit c98f54f
Show file tree
Hide file tree
Showing 11 changed files with 784 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ignore modules pulled in from npm
node_modules/

# rc-apps package output
dist/

# JetBrains IDEs
out/
.idea/
.idea_modules/

# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
64 changes: 64 additions & 0 deletions ClamAvTestApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
IAppAccessors,
IConfigurationExtend,
IEnvironmentRead,
IHttp,
ILogger,
IModify,
IPersistence,
IRead,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { FileUploadNotAllowedException } from '@rocket.chat/apps-engine/definition/exceptions';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import {SettingType} from '@rocket.chat/apps-engine/definition/settings';
import { IFileUploadContext, IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads';

import { createScanner, isCleanReply } from './clamd';

const CLAMAV_SERVER_HOST = 'clamav_server_host';
const CLAMAV_SERVER_PORT = 'clamav_server_port';

export class ClamAvTestApp extends App implements IPreFileUpload {
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors);
}

public async executePreFileUpload(context: IFileUploadContext, read: IRead, http: IHttp, persis: IPersistence, modify: IModify): Promise<void> {
const host = await read.getEnvironmentReader().getSettings().getValueById(CLAMAV_SERVER_HOST);
const port = await read.getEnvironmentReader().getSettings().getValueById(CLAMAV_SERVER_PORT);

if (!host || !port) {
throw new Error('Missing ClamAv connection configuration');
}

const scanner = createScanner(host, port);
const result = await scanner.scanBuffer(context.content);

if (!isCleanReply(result)) {
throw new FileUploadNotAllowedException('Virus found');
}
}

protected async extendConfiguration(configuration: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise<void> {
configuration.settings.provideSetting({
public: true,
id: CLAMAV_SERVER_HOST,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'clamav_server_host_label',
i18nDescription: 'clamav_server_host_description',
required: true,
});

configuration.settings.provideSetting({
public: true,
id: CLAMAV_SERVER_PORT,
type: SettingType.NUMBER,
packageValue: 3310,
i18nLabel: 'clamav_server_port_label',
i18nDescription: 'clamav_server_port_description',
required: true,
});
}
}
21 changes: 21 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "55ecd482-392f-447a-8fdc-a44b371ff794",
"version": "0.0.1",
"requiredApiVersion": "^1.22.1",
"iconFile": "icon.png",
"author": {
"name": "asd",
"homepage": "asd",
"support": "asd"
},
"name": "ClamAVTest",
"nameSlug": "clamavtest",
"classFile": "ClamAvTestApp.ts",
"description": "Test connection with ClamAv",
"implements": [],
"permissions": [
{
"name": "networking"
}
]
}
222 changes: 222 additions & 0 deletions clamd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
'use strict';

/**
* Module dependencies.
*/

import { Buffer } from 'buffer';
import net = require('net');
import { Readable, Stream, Transform } from 'stream';

/**
* Module exports.
*/

export {
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: string, port: number): {
scanStream: typeof scanStream;
scanBuffer: typeof scanBuffer;
} {
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: Readable, timeout?: number): Promise<string> {
if (typeof timeout === 'undefined' || timeout < 0) { timeout = 5000; }

// tslint:disable: only-arrow-functions
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);
});

const replies: Array<Buffer> = [];
socket.setTimeout(timeout as number);
socket
.on('data', function(chunk) {
// clearTimeout(connectAttemptTimer);
if (!readStream.isPaused()) { readStream.pause(); }
replies.push(chunk);
})
.on('end', function() {
// clearTimeout(connectAttemptTimer);
const reply = Buffer.concat(replies);
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: Buffer, timeout?: number, chunkSize?: number): Promise<string> {
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) {
const block = buffer.slice(start, start + size);
this.push(block);
start += block.length;
} else {
this.push(null);
}
},
});
return scanStream(bufReader, timeout);
}

return {
scanStream,
scanBuffer,
};
}

/**
* 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
*/

async function ping(host: string, port: number, timeout: number): Promise<boolean> {
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; }

const res = await _command(host, port, timeout, 'zPING\0');

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
*/

async function version(host: string, port: number, timeout: number): Promise<string> {
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; }

const res = await _command(host, port, timeout, 'zVERSION\0');

return res.toString();
}

/**
* Check the reply mean the file infect or not
*
* @param {*} reply get from the scanner
* @return {boolean}
* @public
*/

function isCleanReply(reply: any): boolean {
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(): Transform {
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: string, port: number, timeout: number, command: string): Promise<Buffer> {
return new Promise(function(resolve, reject) {
const client = net.createConnection({
host,
port,
}, function() {
client.write(command);
});
client.setTimeout(timeout);
const replies: Array<Buffer> = [];
client
.on('data', function(chunk) {
replies.push(chunk);
})
.on('end', function() {
resolve(Buffer.concat(replies));
})
.on('error', reject);
});
}
6 changes: 6 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"clamav_server_host_label": "ClamAV Server Host",
"clamav_server_host_description": "Where your ClamAV server is hosted. E.g.: localhost",
"clamav_server_port_label": "ClamAv Server Port",
"clamav_server_port_description": "The port for your ClamAV server. E.g: 3310"
}
Binary file added icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit c98f54f

Please sign in to comment.