Skip to content

Commit

Permalink
initial checkin
Browse files Browse the repository at this point in the history
  • Loading branch information
msimerson committed May 7, 2024
1 parent 9a66735 commit 6e5a80d
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 154 deletions.
6 changes: 3 additions & 3 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
engines:
eslint:
enabled: true
channel: "eslint-8"
channel: 'eslint-8'
config:
config: ".eslintrc.yaml"
config: '.eslintrc.yaml'

ratings:
paths:
- "**.js"
- '**.js'
2 changes: 1 addition & 1 deletion .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ env:
mocha: true
es2022: true

extends: ["@haraka"]
extends: ['@haraka']
6 changes: 3 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: "monthly"
interval: 'monthly'
allow:
- dependency-type: production
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: "CodeQL"
name: 'CodeQL'

on:
push:
branches: [master]
pull_request:
branches: [master]
schedule:
- cron: "18 7 * * 4"
- cron: '18 7 * * 4'

jobs:
codeql:
Expand Down
2 changes: 2 additions & 0 deletions .prettierrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
singleQuote: true
semi: false
59 changes: 22 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,45 @@

Clone me, to create a new Haraka plugin!

# Template Instructions
# avg - Anti-Virus scanner

These instructions will not self-destruct after use. Use and destroy.
Implement virus scanning with AVG's TCPD daemon, available for Linux/FreeBSD. AVG linux is [free for personal or commercial use](http://www.avg.com/gb-en/faq.pnuid-faq_v3_linux) and can be downloaded from [free.avg.com](http://free.avg.com/gb-en/download.prd-alf).

See also, [How to Write a Plugin](https://github.com/haraka/Haraka/wiki/Write-a-Plugin) and [Plugins.md](https://github.com/haraka/Haraka/blob/master/docs/Plugins.md) for additional plugin writing information.
Messages that AVG detects as infected are rejected. Errors will cause the plugin to return temporary failures unless the defer options are changed (see below).

## Create a new repo for your plugin
## Configuration

Haraka plugins are named like `haraka-plugin-something`. All the namespace after `haraka-plugin-` is yours for the taking. Please check the [Plugins](https://github.com/haraka/Haraka/blob/master/Plugins.md) page and a Google search to see what plugins already exist.
The following options can be set in avg.ini:

Once you've settled on a name, create the GitHub repo. On the repo's main page, click the _Clone or download_ button and copy the URL. Then paste that URL into a local ENV variable with a command like this:
- port (default: 54322)

```sh
export MY_GITHUB_ORG=haraka
export MY_PLUGIN_NAME=haraka-plugin-SOMETHING
```

Clone and rename the avg repo:

```sh
git clone [email protected]:haraka/haraka-plugin-avg.git
mv haraka-plugin-avg $MY_PLUGIN_NAME
cd $MY_PLUGIN_NAME
git remote rm origin
git remote add origin "[email protected]:$MY_GITHUB_ORG/$MY_PLUGIN_NAME.git"
```
TCP port to communicate with the AVG TCPD on.

Now you'll have a local git repo to begin authoring your plugin
- tmpdir (default: /tmp)

## rename boilerplate
AVG TCPD requires that the message be written to disk and scanned. This setting configures where any temporary files are written to. After scanning, the temporary files are automatically removed.

Replaces all uses of the word `avg` with your plugin's name.
- connect_timeout (default: 10)

./redress.sh [something]
Maximum seconds to wait for the socket to connect. Connections taking longer will cause a temporary failure to be sent to the remote MTA.

You'll then be prompted to update package.json and then force push this repo onto the GitHub repo you've created earlier.
- session_timeout

# Add your content here
Maximum number of seconds to wait for a reply to a command before failing. A timeout will cause a temporary failure to be sent to the remote MTA.

## INSTALL
- [defer]

```sh
cd /path/to/local/haraka
npm install haraka-plugin-avg
echo "avg" >> config/plugins
service haraka restart
```

### Configuration
By default, this plugin defers when errors or timeouts are encountered. To
fail open (let messages pass when errors are enounctered), set the error
and/or timeout values to false.

If the default configuration is not sufficient, copy the config file from the distribution into your haraka config dir and then modify it:
[defer]
error=true
timeout=true

```sh
cp node_modules/haraka-plugin-avg/config/avg.ini config/avg.ini
$EDITOR config/avg.ini

```
## USAGE
Expand All @@ -74,3 +58,4 @@ $EDITOR config/avg.ini
[clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-avg
[npm-img]: https://nodei.co/npm/haraka-plugin-avg.png
[npm-url]: https://www.npmjs.com/package/haraka-plugin-avg
```
6 changes: 5 additions & 1 deletion config/avg.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@

[main]
;host=
;port=54322
;tmpdir=/tmp
;connect_timeout=10
;session_timeout=30
167 changes: 152 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,161 @@
"use strict";
// avg - AVG virus scanner
'use strict'

exports.register = function () {
this.load_avg_ini();
const fs = require('node:fs')
const net = require('node:net')
const path = require('node:path')

const net_utils = require('haraka-net-utils')

// register hooks here. More info at https://haraka.github.io/core/Plugins/
// this.register_hook('data_post', 'do_stuff_with_message')
};
const smtp_regexp = /^(\d{3})([ -])(.*)/

exports.register = function () {
this.load_avg_ini()
}

exports.load_avg_ini = function () {
this.cfg = this.config.get(
"avg.ini",
'avg.ini',
{
booleans: [
"+enabled", // this.cfg.main.enabled=true
"-disabled", // this.cfg.main.disabled=false
"+feature_section.yes", // this.cfg.feature_section.yes=true
],
booleans: ['+defer.timeout', '+defer.error'],
},
() => {
this.load_example_ini();
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)
}
22 changes: 13 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "haraka-plugin-template",
"name": "haraka-plugin-avg",
"version": "1.0.6",
"description": "Haraka plugin that...CHANGE THIS",
"description": "Haraka plugin for the AVG virus scanner",
"main": "index.js",
"files": [
"CHANGELOG.md",
Expand All @@ -14,26 +14,30 @@
"prettier": "npx prettier . --check",
"prettier:fix": "npx prettier . --write --log-level=warn",
"test": "node --test",
"versions": "npx @msimerson/dependency-version-checker check",
"versions:fix": "npx @msimerson/dependency-version-checker update"
"versions": "npx dependency-version-checker check",
"versions:fix": "npx dependency-version-checker update"
},
"repository": {
"type": "git",
"url": "git+https://github.com/haraka/haraka-plugin-template.git"
"url": "git+https://github.com/haraka/haraka-plugin-avg.git"
},
"keywords": [
"haraka",
"plugin",
"template"
"avg"
],
"author": "Welcome Member <happy-haraka-hacker@example.com>",
"author": "Welcome Member <haraka.team@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/haraka/haraka-plugin-template/issues"
"url": "https://github.com/haraka/haraka-plugin-avg/issues"
},
"homepage": "https://github.com/haraka/haraka-plugin-template#readme",
"homepage": "https://github.com/haraka/haraka-plugin-avg#readme",
"devDependencies": {
"@haraka/eslint-config": "1.1.3",
"haraka-test-fixtures": "1.3.5"
},
"dependencies": {
"haraka-config": "^1.3.0",
"haraka-net-utils": "^1.7.0"
}
}
Loading

0 comments on commit 6e5a80d

Please sign in to comment.