Socket
Socket
Sign inDemoInstall

haraka-plugin-limit

Package Overview
Dependencies
13
Maintainers
3
Versions
13
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.5 to 1.0.6

26

Changes.md

@@ -1,16 +0,30 @@

## 1.0.5 - 2022-03-08
### 1.0.6 - 2022-05-25
- feat: update redis commands to be v4 compatible
- feat: only load redis when needed, fixes #23
- style: replaced callbacks with async/await in:
get_host_key, get_mail_key, and rate_limit
- dep(eslint): v6 -> v8
- dep(redis): 3 -> 4
- ci: add codeql & publish
### 1.0.5 - 2022-03-08
- fix invalid main field in package.json
## 1.0.4 - 2017-03-23
### 1.0.4 - 2017-03-23
- for outbound, find domain at hmail.todo.domain then hmail.domain.
- noop: use es6 arrow functions
## 1.0.3 - 2017-03-09
### 1.0.3 - 2017-03-09
- add `enabled=false` flag for each limit type, defaults to off, matching the docs.
## 1.0.2 - 2017-02-06
### 1.0.2 - 2017-02-06

@@ -20,6 +34,6 @@ - when redis handle goes away, skip processing

## 1.0.1 - 2017-01-28
### 1.0.1 - 2017-01-28
- increment rate_conn on connect_init
- increment rate_rcpt_host on rcpt/rcpt_ok

@@ -7,54 +7,62 @@ 'use strict';

exports.register = function () {
const plugin = this;
plugin.inherits('haraka-plugin-redis');
this.inherits('haraka-plugin-redis');
plugin.register_hook('init_master', 'init_redis_plugin');
plugin.register_hook('init_child', 'init_redis_plugin');
this.load_limit_ini();
let needs_redis = 0
plugin.load_limit_ini();
if (plugin.cfg.concurrency.enabled) {
plugin.register_hook('connect_init', 'conn_concur_incr');
plugin.register_hook('connect', 'check_concurrency');
plugin.register_hook('disconnect', 'conn_concur_decr');
if (this.cfg.concurrency.enabled) {
this.register_hook('connect_init', 'conn_concur_incr');
this.register_hook('connect', 'check_concurrency');
this.register_hook('disconnect', 'conn_concur_decr');
}
if (plugin.cfg.errors.enabled) {
if (this.cfg.errors.enabled) {
['helo','ehlo','mail','rcpt','data'].forEach(hook => {
plugin.register_hook(hook, 'max_errors');
})
this.register_hook(hook, 'max_errors');
});
}
if (plugin.cfg.recipients.enabled) {
plugin.register_hook('rcpt', 'max_recipients');
if (this.cfg.recipients.enabled) {
this.register_hook('rcpt', 'max_recipients');
}
if (plugin.cfg.unrecognized_commands.enabled) {
plugin.register_hook('unrecognized_command', 'max_unrecognized_commands');
if (this.cfg.unrecognized_commands.enabled) {
this.register_hook('unrecognized_command', 'max_unrecognized_commands');
}
if (plugin.cfg.rate_conn.enabled) {
plugin.register_hook('connect_init', 'rate_conn_incr');
plugin.register_hook('connect', 'rate_conn_enforce');
if (this.cfg.rate_conn.enabled) {
needs_redis++
this.register_hook('connect_init', 'rate_conn_incr');
this.register_hook('connect', 'rate_conn_enforce');
}
if (plugin.cfg.rate_rcpt_host.enabled) {
plugin.register_hook('connect', 'rate_rcpt_host_enforce');
plugin.register_hook('rcpt', 'rate_rcpt_host_incr');
if (this.cfg.rate_rcpt_host.enabled) {
needs_redis++
this.register_hook('connect', 'rate_rcpt_host_enforce');
this.register_hook('rcpt', 'rate_rcpt_host_incr');
}
if (plugin.cfg.rate_rcpt_sender.enabled) {
plugin.register_hook('rcpt', 'rate_rcpt_sender');
if (this.cfg.rate_rcpt_sender.enabled) {
needs_redis++
this.register_hook('rcpt', 'rate_rcpt_sender');
}
if (plugin.cfg.rate_rcpt_null.enabled) {
plugin.register_hook('rcpt', 'rate_rcpt_null');
if (this.cfg.rate_rcpt_null.enabled) {
needs_redis++
this.register_hook('rcpt', 'rate_rcpt_null');
}
if (plugin.cfg.rate_rcpt.enabled) {
plugin.register_hook('rcpt', 'rate_rcpt');
if (this.cfg.rate_rcpt.enabled) {
needs_redis++
this.register_hook('rcpt', 'rate_rcpt');
}
if (plugin.cfg.outbound.enabled) {
plugin.register_hook('send_email', 'outbound_increment');
plugin.register_hook('delivered', 'outbound_decrement');
plugin.register_hook('deferred', 'outbound_decrement');
plugin.register_hook('bounce', 'outbound_decrement');
if (this.cfg.outbound.enabled) {
needs_redis++
this.register_hook('send_email', 'outbound_increment');
this.register_hook('delivered', 'outbound_decrement');
this.register_hook('deferred', 'outbound_decrement');
this.register_hook('bounce', 'outbound_decrement');
}
if (needs_redis) {
this.register_hook('init_master', 'init_redis_plugin');
this.register_hook('init_child', 'init_redis_plugin');
}
}

@@ -81,7 +89,7 @@

if (!plugin.cfg.concurrency) { // no config file
plugin.cfg.concurrency = {};
if (!this.cfg.concurrency) { // no config file
this.cfg.concurrency = {};
}
plugin.merge_redis_ini();
this.merge_redis_ini();
}

@@ -94,11 +102,11 @@

exports.max_unrecognized_commands = function (next, connection, cmd) {
const plugin = this;
if (!plugin.cfg.unrecognized_commands) return next();
connection.results.push(plugin, {unrec_cmds: cmd, emit: true});
if (!this.cfg.unrecognized_commands) return next();
const max = parseFloat(plugin.cfg.unrecognized_commands.max);
connection.results.push(this, {unrec_cmds: cmd, emit: true});
const max = parseFloat(this.cfg.unrecognized_commands.max);
if (!max || isNaN(max)) return next();
const uc = connection.results.get(plugin).unrec_cmds;
const uc = connection.results.get(this).unrec_cmds;
if (!uc || !uc.length) return next();

@@ -108,11 +116,10 @@

connection.results.add(plugin, { fail: 'unrec_cmds.max' });
plugin.penalize(connection, true, 'Too many unrecognized commands', next);
connection.results.add(this, { fail: 'unrec_cmds.max' });
this.penalize(connection, true, 'Too many unrecognized commands', next);
}
exports.max_errors = function (next, connection) {
const plugin = this;
if (!plugin.cfg.errors) return next(); // disabled in config
if (!this.cfg.errors) return next(); // disabled in config
const max = parseFloat(plugin.cfg.errors.max);
const max = parseFloat(this.cfg.errors.max);
if (!max || isNaN(max)) return next();

@@ -122,11 +129,10 @@

connection.results.add(plugin, {fail: 'errors.max'});
plugin.penalize(connection, true, 'Too many errors', next);
connection.results.add(this, {fail: 'errors.max'});
this.penalize(connection, true, 'Too many errors', next);
}
exports.max_recipients = function (next, connection, params) {
const plugin = this;
if (!plugin.cfg.recipients) return next(); // disabled in config
if (!this.cfg.recipients) return next(); // disabled in config
const max = plugin.get_limit('recipients', connection);
const max = this.get_limit('recipients', connection);
if (!max || isNaN(max)) return next();

@@ -138,13 +144,12 @@

connection.results.add(plugin, { fail: 'recipients.max' });
plugin.penalize(connection, false, 'Too many recipient attempts', next);
connection.results.add(this, { fail: 'recipients.max' });
this.penalize(connection, false, 'Too many recipient attempts', next);
}
exports.get_history_limit = function (type, connection) {
const plugin = this;
const history_cfg = `${type}_history`;
if (!plugin.cfg[history_cfg]) return;
if (!this.cfg[history_cfg]) return;
const history_plugin = plugin.cfg[history_cfg].plugin;
const history_plugin = this.cfg[history_cfg].plugin;
if (!history_plugin) return;

@@ -154,4 +159,4 @@

if (!results) {
connection.logerror(plugin, `no ${history_plugin} results, disabling history due to misconfiguration`);
delete plugin.cfg[history_cfg];
connection.logerror(this, `no ${history_plugin} results, disabling history due to misconfiguration`);
delete this.cfg[history_cfg];
return;

@@ -161,3 +166,3 @@ }

if (results.history === undefined) {
connection.logdebug(plugin, `no history from : ${history_plugin}`);
connection.logdebug(this, `no history from : ${history_plugin}`);
return;

@@ -167,58 +172,56 @@ }

const history = parseFloat(results.history);
connection.logdebug(plugin, `history: ${history}`);
connection.logdebug(this, `history: ${history}`);
if (isNaN(history)) return;
if (history > 0) return plugin.cfg[history_cfg].good;
if (history < 0) return plugin.cfg[history_cfg].bad;
return plugin.cfg[history_cfg].none;
if (history > 0) return this.cfg[history_cfg].good;
if (history < 0) return this.cfg[history_cfg].bad;
return this.cfg[history_cfg].none;
}
exports.get_limit = function (type, connection) {
const plugin = this;
if (type === 'recipients') {
if (connection.relaying && plugin.cfg.recipients.max_relaying) {
return plugin.cfg.recipients.max_relaying;
if (connection.relaying && this.cfg.recipients.max_relaying) {
return this.cfg.recipients.max_relaying;
}
}
if (plugin.cfg[`${type}_history`]) {
const history = plugin.get_history_limit(type, connection);
if (this.cfg[`${type}_history`]) {
const history = this.get_history_limit(type, connection);
if (history) return history;
}
return plugin.cfg[type].max || plugin.cfg[type].default;
return this.cfg[type].max || this.cfg[type].default;
}
exports.conn_concur_incr = function (next, connection) {
const plugin = this;
if (!plugin.db) return next();
if (!plugin.cfg.concurrency) return next();
exports.conn_concur_incr = async function (next, connection) {
if (!this.db) return next();
if (!this.cfg.concurrency) return next();
const dbkey = plugin.get_concurrency_key(connection);
const dbkey = this.get_concurrency_key(connection);
plugin.db.incr(dbkey, (err, count) => {
if (err) {
connection.results.add(plugin, { err: `conn_concur_incr:${err}` });
return next();
}
try {
const count = await this.db.incr(dbkey)
if (isNaN(count)) {
connection.results.add(plugin, {err: 'conn_concur_incr got isNaN'});
connection.results.add(this, {err: 'conn_concur_incr got isNaN'});
return next();
}
connection.results.add(plugin, { concurrent_count: count });
connection.results.add(this, { concurrent_count: count });
// repair negative concurrency counters
if (count < 1) {
connection.results.add(plugin, {
connection.results.add(this, {
msg: `resetting concurrent ${count} to 1`
});
plugin.db.set(dbkey, 1);
this.db.set(dbkey, 1);
}
plugin.db.expire(dbkey, 3 * 60); // 3 minute lifetime
next();
});
this.db.expire(dbkey, 3 * 60); // 3 minute lifetime
}
catch (err) {
connection.results.add(this, { err: `conn_concur_incr:${err}` });
}
next();
}

@@ -231,35 +234,31 @@

exports.check_concurrency = function (next, connection) {
const plugin = this;
const max = plugin.get_limit('concurrency', connection);
const max = this.get_limit('concurrency', connection);
if (!max || isNaN(max)) {
connection.results.add(plugin, {err: "concurrency: no limit?!"});
connection.results.add(this, {err: "concurrency: no limit?!"});
return next();
}
const count = parseInt(connection.results.get(plugin.name).concurrent_count);
const count = parseInt(connection.results.get(this.name).concurrent_count);
if (isNaN(count)) {
connection.results.add(plugin, { err: 'concurrent.unset' });
connection.results.add(this, { err: 'concurrent.unset' });
return next();
}
connection.results.add(plugin, { concurrent: `${count}/${max}` });
connection.results.add(this, { concurrent: `${count}/${max}` });
if (count <= max) return next();
connection.results.add(plugin, { fail: 'concurrency.max' });
connection.results.add(this, { fail: 'concurrency.max' });
plugin.penalize(connection, true, 'Too many concurrent connections', next);
this.penalize(connection, true, 'Too many concurrent connections', next);
}
exports.penalize = function (connection, disconnect, msg, next) {
const plugin = this;
const code = disconnect ? constants.DENYSOFTDISCONNECT : constants.DENYSOFT;
if (!plugin.cfg.main.tarpit_delay) {
return next(code, msg);
}
if (!this.cfg.main.tarpit_delay) return next(code, msg);
const delay = plugin.cfg.main.tarpit_delay;
connection.loginfo(plugin, `tarpitting for ${delay}s`);
const delay = this.cfg.main.tarpit_delay;
connection.loginfo(this, `tarpitting for ${delay}s`);

@@ -272,18 +271,22 @@ setTimeout(() => {

exports.conn_concur_decr = function (next, connection) {
const plugin = this;
if (!plugin.db) return next();
if (!plugin.cfg.concurrency) return next();
exports.conn_concur_decr = async function (next, connection) {
const dbkey = plugin.get_concurrency_key(connection);
plugin.db.incrby(dbkey, -1, (err, concurrent) => {
if (err) connection.results.add(plugin, { err: `conn_concur_decr:${err}` })
return next();
});
if (!this.db) return next();
if (!this.cfg.concurrency) return next();
try {
const dbkey = this.get_concurrency_key(connection);
await this.db.incrby(dbkey, -1)
}
catch (err) {
connection.results.add(this, { err: `conn_concur_decr:${err}` })
}
next();
}
exports.get_host_key = function (type, connection, cb) {
const plugin = this;
if (!plugin.cfg[type]) {
return cb(new Error(`${type}: not configured`));
exports.get_host_key = function (type, connection) {
if (!this.cfg[type]) {
connection.results.add(this, { err: `${type}: not configured` });
return
}

@@ -302,3 +305,4 @@

catch (err) {
return cb(err);
connection.results.add(this, { err: `${type}: ${err.message}` });
return
}

@@ -309,4 +313,4 @@

const part = ((ip.kind === 'ipv6') ? ip_array.join(':') : ip_array.join('.'));
if (plugin.cfg[type][part] || plugin.cfg[type][part] === 0) {
return cb(null, part, plugin.cfg[type][part]);
if (this.cfg[type][part] || this.cfg[type][part] === 0) {
return [ part, this.cfg[type][part] ]
}

@@ -321,4 +325,4 @@ ip_array.pop();

const part2 = rdns_array.join('.');
if (plugin.cfg[type][part2] || plugin.cfg[type][part2] === 0) {
return cb(null, part2, plugin.cfg[type][part2]);
if (this.cfg[type][part2] || this.cfg[type][part2] === 0) {
return [ part2, this.cfg[type][part2] ]
}

@@ -329,24 +333,23 @@ rdns_array.pop();

if (plugin.cfg[`${type}_history`]) {
const history = plugin.get_history_limit(type, connection);
if (history) return cb(null, ip, history);
if (this.cfg[`${type}_history`]) {
const history = this.get_history_limit(type, connection);
if (history) return [ ip, history ]
}
// Custom Default
if (plugin.cfg[type].default) {
return cb(null, ip, plugin.cfg[type].default);
if (this.cfg[type].default) {
return [ ip, this.cfg[type].default ]
}
// Default 0 = unlimited
cb(null, ip, 0);
return [ ip, 0 ]
}
exports.get_mail_key = function (type, mail, cb) {
const plugin = this;
if (!plugin.cfg[type] || !mail) return cb();
exports.get_mail_key = function (type, mail) {
if (!this.cfg[type] || !mail) return;
// Full e-mail address (e.g. smf@fsl.com)
const email = mail.address();
if (plugin.cfg[type][email] || plugin.cfg[type][email] === 0) {
return cb(email, plugin.cfg[type][email]);
if (this.cfg[type][email] || this.cfg[type][email] === 0) {
return [ email, this.cfg[type][email] ]
}

@@ -359,4 +362,4 @@

const part = rhs_split.join('.');
if (plugin.cfg[type][part] || plugin.cfg[type][part] === 0) {
return cb(part, plugin.cfg[type][part]);
if (this.cfg[type][part] || this.cfg[type][part] === 0) {
return [ part, this.cfg[type][part] ]
}

@@ -368,8 +371,8 @@ rhs_split.pop();

// Custom Default
if (plugin.cfg[type].default) {
return cb(email, plugin.cfg[type].default);
if (this.cfg[type].default) {
return [ email, this.cfg[type].default ]
}
// Default 0 = unlimited
cb(email, 0);
return [ email, 0 ]
}

@@ -402,3 +405,3 @@

default:
return;
return ttl;
}

@@ -414,13 +417,12 @@ return ttl;

exports.rate_limit = function (connection, key, value, cb) {
const plugin = this;
exports.rate_limit = async function (connection, key, value) {
if (value === 0) { // Limit disabled for this host
connection.loginfo(this, `rate limit disabled for: ${key}`);
return cb(null, false);
return false
}
// CAUTION: !value would match that 0 value -^
if (!key || !value) return cb();
if (!plugin.db) return cb();
if (!key || !value) return
if (!this.db) return

@@ -431,154 +433,136 @@ const limit = getLimit(value);

if (!limit || ! ttl) {
return cb(new Error(`syntax error: key=${key} value=${value}`));
connection.results.add(this, { err: `syntax error: key=${key} value=${value}` });
return
}
connection.logdebug(plugin, `key=${key} limit=${limit} ttl=${ttl}`);
connection.logdebug(this, `key=${key} limit=${limit} ttl=${ttl}`);
plugin.db.incr(key, (err, newval) => {
if (err) return cb(err);
if (newval === 1) plugin.db.expire(key, ttl);
cb(err, parseInt(newval, 10) > limit); // boolean true/false
})
try {
const newval = await this.db.incr(key)
if (newval === 1) this.db.expire(key, ttl);
return parseInt(newval, 10) > limit // boolean
}
catch (err) {
connection.results.add(this, { err: `${key}:${err}` });
}
}
exports.rate_rcpt_host_incr = function (next, connection) {
const plugin = this;
if (!plugin.db) return next();
exports.rate_rcpt_host_incr = async function (next, connection) {
if (!this.db) return next();
plugin.get_host_key('rate_rcpt_host', connection, (err, key, value) => {
if (!key || !value) return next();
const [ key, value ] = this.get_host_key('rate_rcpt_host', connection)
if (!key || !value) return next();
key = `rate_rcpt_host:${key}`;
plugin.db.incr(key, (err2, newval) => {
if (newval === 1) plugin.db.expire(key, getTTL(value));
next();
})
})
try {
const newval = await this.db.incr(`rate_rcpt_host:${key}`)
if (newval === 1) await this.db.expire(`rate_rcpt_host:${key}`, getTTL(value));
}
catch (err) {
connection.results.add(this, { err })
}
next();
}
exports.rate_rcpt_host_enforce = function (next, connection) {
const plugin = this;
if (!plugin.db) return next();
exports.rate_rcpt_host_enforce = async function (next, connection) {
if (!this.db) return next();
plugin.get_host_key('rate_rcpt_host', connection, (err, key, value) => {
if (err) {
connection.results.add(plugin, { err: `rate_rcpt_host:${err}` });
return next();
}
const [ key, value ] = this.get_host_key('rate_rcpt_host', connection)
if (!key || !value) return next();
if (!key || !value) return next();
const match = /^(\d+)/.exec(value);
const limit = parseInt(match[0], 10);
if (!limit) return next();
const match = /^(\d+)/.exec(value);
const limit = parseInt(match[0], 10);
if (!limit) return next();
try {
const result = await this.db.get(`rate_rcpt_host:${key}`)
plugin.db.get(`rate_rcpt_host:${key}`, (err2, result) => {
if (err2) {
connection.results.add(plugin, { err: `rate_rcpt_host:${err2}` });
return next();
}
if (!result) return next();
connection.results.add(this, {
rate_rcpt_host: `${key}:${result}:${value}`
});
if (!result) return next();
connection.results.add(plugin, {
rate_rcpt_host: `${key}:${result}:${value}`
});
if (result <= limit) return next();
if (result <= limit) return next();
connection.results.add(plugin, { fail: 'rate_rcpt_host' });
plugin.penalize(connection, false, 'recipient rate limit exceeded', next);
});
});
connection.results.add(this, { fail: 'rate_rcpt_host' });
this.penalize(connection, false, 'recipient rate limit exceeded', next);
}
catch (err) {
connection.results.add(this, { err: `rate_rcpt_host:${err}` });
next();
}
}
exports.rate_conn_incr = function (next, connection) {
const plugin = this;
if (!plugin.db) return next();
exports.rate_conn_incr = async function (next, connection) {
if (!this.db) return next();
plugin.get_host_key('rate_conn', connection, (err, key, value) => {
if (!key || !value) return next();
const [ key, value ] = this.get_host_key('rate_conn', connection)
if (!key || !value) return next();
key = `rate_conn:${key}`;
plugin.db.hincrby(key, + new Date(), 1, (err2, newval) => {
if (err2) connection.results.add(plugin, { err: err2 });
// extend key expiration on every new connection
plugin.db.expire(key, getTTL(value) * 2);
next();
});
});
try {
await this.db.hIncrBy(`rate_conn:${key}`, (+ new Date()).toString(), 1)
// extend key expiration on every new connection
await this.db.expire(`rate_conn:${key}`, getTTL(value) * 2)
}
catch (err) {
console.error(err)
connection.results.add(this, { err });
}
next()
}
exports.rate_conn_enforce = function (next, connection) {
const plugin = this;
if (!plugin.db) return next();
exports.rate_conn_enforce = async function (next, connection) {
if (!this.db) return next();
plugin.get_host_key('rate_conn', connection, (err, key, value) => {
if (err) {
connection.results.add(plugin, { err: `rate_conn:${err}` });
return next();
}
const [ key, value ] = this.get_host_key('rate_conn', connection)
if (!key || !value) return next();
if (!key || !value) return next();
const limit = getLimit(value);
if (!limit) {
connection.results.add(this, { err: `rate_conn:syntax:${value}` });
return next();
}
const limit = getLimit(value);
if (!limit) {
connection.results.add(plugin, { err: `rate_conn:syntax:${value}` });
try {
const tstamps = await this.db.hGetAll(`rate_conn:${key}`)
if (!tstamps) {
connection.results.add(this, { err: 'rate_conn:no_tstamps' });
return next();
}
plugin.db.hgetall(`rate_conn:${key}`, (err2, tstamps) => {
if (err2) {
connection.results.add(plugin, { err: `rate_conn:${err}` });
return next();
}
const d = new Date();
d.setMinutes(d.getMinutes() - (getTTL(value) / 60));
const periodStartTs = + d; // date as integer
if (!tstamps) {
connection.results.add(plugin, { err: 'rate_conn:no_tstamps' });
return next();
}
let connections_in_ttl_period = 0;
Object.keys(tstamps).forEach(ts => {
if (parseInt(ts, 10) < periodStartTs) return; // older than ttl
connections_in_ttl_period = connections_in_ttl_period + parseInt(tstamps[ts], 10);
})
connection.results.add(this, { rate_conn: `${connections_in_ttl_period}:${value}`});
const d = new Date();
d.setMinutes(d.getMinutes() - (getTTL(value) / 60));
const periodStartTs = + d; // date as integer
if (connections_in_ttl_period <= limit) return next();
let connections_in_ttl_period = 0;
Object.keys(tstamps).forEach(ts => {
if (parseInt(ts, 10) < periodStartTs) return; // older than ttl
connections_in_ttl_period = connections_in_ttl_period + parseInt(tstamps[ts], 10);
})
connection.results.add(plugin, { rate_conn: `${connections_in_ttl_period}:${value}`});
connection.results.add(this, { fail: 'rate_conn' });
if (connections_in_ttl_period <= limit) return next();
connection.results.add(plugin, { fail: 'rate_conn' });
plugin.penalize(connection, true, 'connection rate limit exceeded', next);
});
});
this.penalize(connection, true, 'connection rate limit exceeded', next);
}
catch (err) {
connection.results.add(this, { err: `rate_conn:${err}` });
next();
}
}
exports.rate_rcpt_sender = function (next, connection, params) {
const plugin = this;
exports.rate_rcpt_sender = async function (next, connection, params) {
plugin.get_mail_key('rate_rcpt_sender', connection.transaction.mail_from, (key, value) => {
const [ key, value ] = this.get_mail_key('rate_rcpt_sender', connection.transaction.mail_from)
connection.results.add(this, { rate_rcpt_sender: value });
plugin.rate_limit(connection, `rate_rcpt_sender:${key}`, value, (err, over) => {
if (err) {
connection.results.add(plugin, { err: `rate_rcpt_sender:${err}` });
return next();
}
const over = await this.rate_limit(connection, `rate_rcpt_sender:${key}`, value)
if (!over) return next();
connection.results.add(plugin, { rate_rcpt_sender: value });
if (!over) return next();
connection.results.add(plugin, { fail: 'rate_rcpt_sender' });
plugin.penalize(connection, false, 'rcpt rate limit exceeded', next);
});
});
connection.results.add(this, { fail: 'rate_rcpt_sender' });
this.penalize(connection, false, 'rcpt rate limit exceeded', next);
}
exports.rate_rcpt_null = function (next, connection, params) {
const plugin = this;
exports.rate_rcpt_null = async function (next, connection, params) {

@@ -590,39 +574,24 @@ if (!params) return next();

// Message from the null sender
plugin.get_mail_key('rate_rcpt_null', params, (key, value) => {
const [ key, value ] = this.get_mail_key('rate_rcpt_null', params)
connection.results.add(this, { rate_rcpt_null: value });
plugin.rate_limit(connection, `rate_rcpt_null:${key}`, value, (err2, over) => {
if (err2) {
connection.results.add(plugin, { err: `rate_rcpt_null:${err2}` });
return next();
}
const over = await this.rate_limit(connection, `rate_rcpt_null:${key}`, value)
if (!over) return next();
connection.results.add(plugin, { rate_rcpt_null: value });
if (!over) return next();
connection.results.add(plugin, { fail: 'rate_rcpt_null' });
plugin.penalize(connection, false, 'null recip rate limit', next);
});
});
connection.results.add(this, { fail: 'rate_rcpt_null' });
this.penalize(connection, false, 'null recip rate limit', next);
}
exports.rate_rcpt = function (next, connection, params) {
exports.rate_rcpt = async function (next, connection, params) {
const plugin = this;
if (Array.isArray(params)) params = params[0];
plugin.get_mail_key('rate_rcpt', params, (key, value) => {
plugin.rate_limit(connection, `rate_rcpt:${key}`, value, (err2, over) => {
if (err2) {
connection.results.add(plugin, { err: `rate_rcpt:${err2}` });
return next();
}
const [ key, value ] = plugin.get_mail_key('rate_rcpt', params)
connection.results.add(plugin, { rate_rcpt: value });
connection.results.add(plugin, { rate_rcpt: value });
const over = await plugin.rate_limit(connection, `rate_rcpt:${key}`, value)
if (!over) return next();
if (!over) return next();
connection.results.add(plugin, { fail: 'rate_rcpt' });
plugin.penalize(connection, false, 'rate limit exceeded', next);
});
});
connection.results.add(plugin, { fail: 'rate_rcpt' });
plugin.penalize(connection, false, 'rate limit exceeded', next);
}

@@ -636,7 +605,5 @@

function getOutDom (hmail) {
// outbound isn't internally consistent in the use of hmail.domain
// vs hmail.todo.domain.
// outbound isn't internally consistent using hmail.domain and hmail.todo.domain.
// TODO: fix haraka/Haraka/outbound/HMailItem to be internally consistent.
if (hmail.todo && hmail.todo.domain) return hmail.todo.domain;
return hmail.domain;
return hmail?.todo?.domain || hmail.domain;
}

@@ -648,5 +615,4 @@

exports.outbound_increment = function (next, hmail) {
const plugin = this;
if (!plugin.db) return next();
exports.outbound_increment = async function (next, hmail) {
if (!this.db) return next();

@@ -656,13 +622,9 @@ const outDom = getOutDom(hmail);

plugin.db.hincrby(outKey, 'TOTAL', 1, (err, count) => {
if (err) {
plugin.logerror(`outbound_increment: ${err}`);
return next(); // just deliver
}
try {
let count = await this.db.hIncrBy(outKey, 'TOTAL', 1)
this.db.expire(outKey, 300); // 5 min expire
plugin.db.expire(outKey, 300); // 5 min expire
if (!plugin.cfg.outbound[outDom]) return next();
const limit = parseInt(plugin.cfg.outbound[outDom], 10);
if (!this.cfg.outbound[outDom]) return next();
const limit = parseInt(this.cfg.outbound[outDom], 10);
if (!limit) return next();

@@ -673,13 +635,16 @@

const delay = plugin.cfg.outbound.delay || 30;
const delay = this.cfg.outbound.delay || 30;
next(constants.delay, delay);
})
}
catch (err) {
this.logerror(`outbound_increment: ${err}`);
next(); // just deliver
}
}
exports.outbound_decrement = function (next, hmail) {
const plugin = this;
if (!plugin.db) return next();
if (!this.db) return next();
plugin.db.hincrby(getOutKey(getOutDom(hmail)), 'TOTAL', -1);
return next();
this.db.hIncrBy(getOutKey(getOutDom(hmail)), 'TOTAL', -1);
next();
}
{
"name": "haraka-plugin-limit",
"version": "1.0.5",
"version": "1.0.6",
"description": "enforce various types of limits on remote MTAs",

@@ -11,16 +11,16 @@ "main": "index.js",

"haraka-constants": "*",
"haraka-plugin-redis": "*",
"ipaddr.js": "^2.0.0",
"redis": "^3.1.1"
"haraka-plugin-redis": "2",
"ipaddr.js": "^2.0.1",
"redis": "4"
},
"devDependencies": {
"address-rfc2821": "*",
"eslint": ">=6",
"eslint": "8",
"eslint-plugin-haraka": "*",
"haraka-test-fixtures": "*",
"mocha": "*"
"mocha": "9"
},
"scripts": {
"lint": "npx eslint *.js test/*.js",
"lintfix": "npx eslint --fix *.js test/*.js",
"lint": "npx eslint *.js test",
"lintfix": "npx eslint --fix *.js test",
"versions": "npx dependency-version-checker check",

@@ -27,0 +27,0 @@ "test": "npx mocha --exit"

@@ -237,6 +237,4 @@ # limit

[ci-img]: https://github.com/haraka/haraka-plugin-limit/actions/workflows/ci-test.yml/badge.svg
[ci-url]: https://github.com/haraka/haraka-plugin-limit/actions/workflows/ci-test.yml
[ci-lint-img]: https://github.com/haraka/haraka-plugin-limit/actions/workflows/lint.yml/badge.svg
[ci-lint-url]: https://github.com/haraka/haraka-plugin-limit/actions/workflows/lint.yml
[ci-img]: https://github.com/haraka/haraka-plugin-limit/actions/workflows/ci.yml/badge.svg
[ci-url]: https://github.com/haraka/haraka-plugin-limit/actions/workflows/ci.yml
[clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-limit/badges/gpa.svg

@@ -243,0 +241,0 @@ [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-limit

@@ -19,3 +19,3 @@ 'use strict';

rate_rcpt_null: { enabled: false, default: 1 },
redis: { db: 4, host: '127.0.0.1', port: '6379' },
redis: { db: 4, socket: { host: '127.0.0.1', port: '6379' } },
concurrency: { plugin: 'karma', good: 10, bad: 1, none: 2 }

@@ -26,8 +26,8 @@ };

before(function (done) {
before(function () {
this.plugin = new fixtures.plugin('index');
this.plugin.config = this.plugin.config.module_config(path.resolve('test'));
done()
})
it('loads config', function (done) {
it('loads config', function () {
// gotta inherit b/c config loader merges in defaults from redis.ini

@@ -37,10 +37,8 @@ this.plugin.inherits('haraka-plugin-redis');

assert.deepEqual(this.plugin.cfg, default_config); // loaded config
done()
})
it('registers', function (done) {
it('registers', function () {
this.plugin.register();
assert.deepEqual(this.plugin.cfg, default_config); // loaded config
done();
assert.deepEqual(this.plugin.cfg, default_config);
})
})

@@ -11,3 +11,3 @@ 'use strict';

before(function (done) {
before(function () {
this.plugin = new fixtures.plugin('index');

@@ -27,6 +27,5 @@ this.plugin.config = this.plugin.config.module_config(path.resolve('test'));

};
done();
})
it('good', function (done) {
it('good', function () {
this.connection.results.add({name: 'karma'}, { history: 1 });

@@ -37,6 +36,5 @@ assert.equal(

);
done();
})
it('bad', function (done) {
it('bad', function () {
this.connection.results.add({name: 'karma'}, { history: -1 });

@@ -47,6 +45,5 @@ assert.equal(

);
done();
})
it('none', function (done) {
it('none', function () {
this.connection.results.add({name: 'karma'}, { history: 0 });

@@ -57,4 +54,3 @@ assert.equal(

);
done();
})
})

@@ -9,25 +9,21 @@ 'use strict';

beforeEach(function (done) {
beforeEach(function () {
this.plugin = new fixtures.plugin('index');
done();
})
it('inherits redis', function (done) {
it('inherits redis', function () {
this.plugin.inherits('haraka-plugin-redis');
assert.equal(typeof this.plugin.load_redis_ini, 'function');
done();
})
it('can call parent functions', function (done) {
it('can call parent functions', function () {
this.plugin.inherits('haraka-plugin-redis');
this.plugin.load_redis_ini();
assert.ok(this.plugin.redisCfg); // loaded config
done();
})
it('register', function (done) {
it('register', function () {
this.plugin.register();
assert.ok(this.plugin.cfg); // loaded config
done();
})
})

@@ -9,3 +9,3 @@ 'use strict';

function setUp (done) {
function setUp () {
this.plugin = new fixtures.plugin('index');

@@ -18,3 +18,2 @@ this.plugin.config = this.plugin.config.module_config(path.resolve('test'));

this.plugin.register();
done()
}

@@ -21,0 +20,0 @@

@@ -11,8 +11,5 @@ 'use strict';

this.plugin = new fixtures.plugin('index');
// gotta inhert b/c config loader merges in defaults from redis.ini
// this.plugin.inherits('haraka-plugin-redis');
this.plugin.register();
this.server = { notes: {} };
this.plugin.init_redis_plugin(function () {
// console.log(arguments);
done();

@@ -38,3 +35,3 @@ },

self.plugin.cfg.outbound['slow.test.com'] = 3;
self.plugin.db.hset('outbound-rate:slow.test.com', 'TOTAL', 4, function () {
self.plugin.db.hSet('outbound-rate:slow.test.com', 'TOTAL', 4).then(() => {
self.plugin.outbound_increment(function (code, delay) {

@@ -41,0 +38,0 @@ assert.equal(code, constants.delay);

@@ -10,3 +10,3 @@ 'use strict';

function setUp (done) {
function setUp () {
this.plugin = new fixtures.plugin('rate_limit');

@@ -18,3 +18,2 @@

this.plugin.register();
done();
}

@@ -24,18 +23,12 @@

before(setUp)
it('rate_conn', function (done) {
this.plugin.get_host_key('rate_conn', this.connection, function (err, ip, limit) {
assert.equal(err, undefined);
assert.equal(ip, '1.2.3.4');
assert.equal(limit, 5);
done();
})
it('rate_conn', function () {
const [ ip, limit ] = this.plugin.get_host_key('rate_conn', this.connection)
assert.equal(ip, '1.2.3.4');
assert.equal(limit, 5);
})
it('rate_rcpt_host', function (done) {
this.plugin.get_host_key('rate_rcpt_host', this.connection, function (err, ip, limit) {
assert.equal(err, undefined);
assert.equal(ip, '1.2.3.4');
assert.equal(limit, '50/5m');
done();
})
it('rate_rcpt_host', function () {
const [ ip, limit ] = this.plugin.get_host_key('rate_rcpt_host', this.connection)
assert.equal(ip, '1.2.3.4');
assert.equal(limit, '50/5m');
})

@@ -45,32 +38,25 @@ })

describe('get_mail_key', function () {
beforeEach(function (done) {
beforeEach(function () {
this.plugin = new fixtures.plugin('rate_limit');
this.connection = new fixtures.connection.createConnection();
this.plugin.register();
done();
})
it('rate_rcpt_sender', function (done) {
this.plugin.get_mail_key('rate_rcpt_sender', new Address('<user@example.com>'), function (addr, limit) {
// console.log(arguments);
assert.equal(addr, 'user@example.com');
assert.equal(limit, '50/5m');
done();
});
it('rate_rcpt_sender', function () {
const [ addr, limit ] = this.plugin.get_mail_key('rate_rcpt_sender', new Address('<user@example.com>'))
// console.log(arguments);
assert.equal(addr, 'user@example.com');
assert.equal(limit, '50/5m');
})
it('rate_rcpt_null', function (done) {
this.plugin.get_mail_key('rate_rcpt_null', new Address('<postmaster>'), function (addr, limit) {
// console.log(arguments);
assert.equal(addr, 'postmaster');
assert.equal(limit, '1');
done();
});
it('rate_rcpt_null', function () {
const [ addr, limit ] = this.plugin.get_mail_key('rate_rcpt_null', new Address('<postmaster>'))
// console.log(arguments);
assert.equal(addr, 'postmaster');
assert.equal(limit, '1');
})
it('rate_rcpt', function (done) {
this.plugin.get_mail_key('rate_rcpt', new Address('<user@example.com>'), function (addr, limit) {
// console.log(arguments);
assert.equal(addr, 'user@example.com');
assert.equal(limit, '50/5m');
done();
});
it('rate_rcpt', function () {
const [ addr, limit ] = this.plugin.get_mail_key('rate_rcpt', new Address('<user@example.com>'))
// console.log(arguments);
assert.equal(addr, 'user@example.com');
assert.equal(limit, '50/5m');
})

@@ -91,18 +77,10 @@ })

it('no limit', function (done) {
this.plugin.rate_limit(this.connection, 'key', 0, function (err, is_limited) {
// console.log(arguments);
assert.equal(err, undefined);
assert.equal(is_limited, false);
done();
})
it('no limit', async function () {
const is_limited = await this.plugin.rate_limit(this.connection, 'key', 0)
assert.equal(is_limited, false);
})
it('below 50/5m limit', function (done) {
this.plugin.rate_limit(this.connection, 'key', '50/5m', function (err, is_limited) {
// console.log(arguments);
assert.equal(err, undefined);
assert.equal(is_limited, false);
done();
})
it('below 50/5m limit', async function () {
const is_limited = await this.plugin.rate_limit(this.connection, 'key', '50/5m')
assert.equal(is_limited, false);
})

@@ -113,2 +91,4 @@ })

beforeEach(function (done) {
this.server = { notes: {} };
this.plugin = new fixtures.plugin('rate_limit');

@@ -122,7 +102,6 @@ this.plugin.config = this.plugin.config.module_config(path.resolve('test'));

this.plugin.register();
const server = { notes: {} };
this.plugin.init_redis_plugin(function () {
done();
},
server);
this.server);
})

@@ -129,0 +108,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc