haraka-plugin-limit
Advanced tools
Comparing version 1.0.5 to 1.0.6
@@ -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 | ||
575
index.js
@@ -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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Floating dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
1
3
47497
1280
15
862
243
1
- Removeddenque@1.5.1(transitive)
- Removedredis@3.1.2(transitive)
- Removedredis-commands@1.7.0(transitive)
- Removedredis-errors@1.2.0(transitive)
- Removedredis-parser@3.0.0(transitive)
Updatedharaka-plugin-redis@2
Updatedipaddr.js@^2.0.1
Updatedredis@4