Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

haraka-plugin-limit

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

haraka-plugin-limit - npm Package Compare versions

Comparing version 1.0.0 to 1.0.1

Changes.md

26

.eslintrc.json
{
"env": {
"node": true,
"mocha": true,
"es6": true
},
"extends": "eslint:recommended",
"installedESLint": true,
"root": true,
"plugins": [
"haraka"
],
"extends": ["eslint:recommended", "plugin:haraka/recommended"],
"rules": {
"comma-dangle": [2, "only-multiline"],
"dot-notation": 2,
"indent": [2, 4, {"SwitchCase": 1}],
"one-var": [2, "never"],
"no-trailing-spaces": [2, { "skipBlankLines": false }],
"keyword-spacing": [2, {
"before": true,
"after": true
}],
"no-delete-var": 2,
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-label-var": 2,
"no-shadow": 2,
"no-unused-vars": [ 1, { "args": "none" }],
"no-console": 0
}
}

@@ -16,5 +16,5 @@ 'use strict';

if (plugin.cfg.concurrency) {
plugin.register_hook('connect_init', 'incr_concurrency');
plugin.register_hook('connect_init', 'conn_concur_incr');
plugin.register_hook('connect', 'check_concurrency');
plugin.register_hook('disconnect', 'decr_concurrency');
plugin.register_hook('disconnect', 'conn_concur_decr');
}

@@ -36,16 +36,25 @@

plugin.register_hook('connect', 'rate_rcpt_host');
plugin.register_hook('connect', 'rate_conn');
if (plugin.cfg.rate_conn) {
plugin.register_hook('connect_init', 'rate_conn_incr');
plugin.register_hook('connect', 'rate_conn_enforce');
}
if (plugin.cfg.rate_rcpt_host) {
plugin.register_hook('connect', 'rate_rcpt_host_enforce');
plugin.register_hook('rcpt', 'rate_rcpt_host_incr');
}
if (plugin.cfg.rate_rcpt_sender) {
plugin.register_hook('rcpt', 'rate_rcpt_sender');
}
if (plugin.cfg.rate_rcpt_null) {
plugin.register_hook('rcpt', 'rate_rcpt_null');
}
if (plugin.cfg.rate_rcpt) {
plugin.register_hook('rcpt', 'rate_rcpt');
}
['rcpt', 'rcpt_ok'].forEach(function (h) {
plugin.register_hook(h, 'rate_rcpt_sender');
plugin.register_hook(h, 'rate_rcpt_null');
plugin.register_hook(h, 'rate_rcpt');
});
if (plugin.cfg.outbound.enabled) {
plugin.register_hook('send_email', 'increment_outbound_limit');
plugin.register_hook('delivered', 'decrement_outbound_limit');
plugin.register_hook('deferred', 'decrement_outbound_limit');
plugin.register_hook('bounce', 'decrement_outbound_limit');
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');
}

@@ -76,3 +85,3 @@ };

exports.max_unrecognized_commands = function(next, connection, cmd) {
exports.max_unrecognized_commands = function (next, connection, cmd) {
var plugin = this;

@@ -91,3 +100,3 @@ if (!plugin.cfg.unrecognized_commands) return next();

connection.results.add(plugin, {fail: 'unrec_cmds.max'});
connection.results.add(plugin, { fail: 'unrec_cmds.max' });
plugin.penalize(connection, true, 'Too many unrecognized commands', next);

@@ -113,3 +122,3 @@ };

var max = plugin.get_recipient_limit(connection);
var max = plugin.get_limit('recipients', connection);
if (!max || isNaN(max)) return next();

@@ -125,13 +134,10 @@

exports.get_recipient_limit = function (connection) {
exports.get_history_limit = function (type, connection) {
var plugin = this;
if (connection.relaying && plugin.cfg.recipients.max_relaying) {
return plugin.cfg.recipients.max_relaying;
}
var history_cfg = type + '_history';
if (!plugin.cfg[history_cfg]) return;
var history_plugin = plugin.cfg.concurrency.history;
if (!history_plugin) {
return plugin.cfg.recipients.max;
}
var history_plugin = plugin.cfg[history_cfg].plugin;
if (!history_plugin) return;

@@ -142,9 +148,9 @@ var results = connection.results.get(history_plugin);

' disabling history due to misconfiguration');
delete plugin.cfg.recipients.history;
return plugin.cfg.recipients.max;
delete plugin.cfg[history_cfg];
return;
}
if (results.history === undefined) {
connection.logerror(plugin, 'no history from : ' + history_plugin);
return plugin.cfg.recipients.max;
connection.logdebug(plugin, 'no history from : ' + history_plugin);
return;
}

@@ -154,18 +160,36 @@

connection.logdebug(plugin, 'history: ' + history);
if (isNaN(history)) { history = 0; }
if (isNaN(history)) return;
if (history > 0) return plugin.cfg.recipients.history_good || 50;
if (history < 0) return plugin.cfg.recipients.history_bad || 2;
return plugin.cfg.recipients.history_none || 15;
if (history > 0) return plugin.cfg[history_cfg].good;
if (history < 0) return plugin.cfg[history_cfg].bad;
return plugin.cfg[history_cfg].none;
};
exports.incr_concurrency = function (next, connection) {
exports.get_limit = function (type, connection) {
var plugin = this;
if (type === 'recipients') {
if (connection.relaying && plugin.cfg.recipients.max_relaying) {
return plugin.cfg.recipients.max_relaying;
}
}
if (plugin.cfg[type + '_history']) {
var history = plugin.get_history_limit(type, connection);
if (history) return history;
}
return plugin.cfg[type].max || plugin.cfg[type].default;
};
exports.conn_concur_incr = function (next, connection) {
var plugin = this;
if (!plugin.db) return next();
if (!plugin.cfg.concurrency) return next();
var dbkey = plugin.get_key(connection);
var dbkey = plugin.get_concurrency_key(connection);
plugin.db.incr(dbkey, function (err, count) {
if (err) {
connection.results.add(plugin, { err: 'incr_concurrency:' + err });
connection.results.add(plugin, { err: 'conn_concur_incr:' + err });
return next();

@@ -175,3 +199,3 @@ }

if (isNaN(count)) {
connection.results.add(plugin, {err: 'incr_concurrency got isNaN'});
connection.results.add(plugin, {err: 'conn_concur_incr got isNaN'});
return next();

@@ -190,3 +214,3 @@ }

plugin.db.expire(dbkey, 120); // 2 minute lifetime
plugin.db.expire(dbkey, 3 * 60); // 3 minute lifetime
next();

@@ -196,3 +220,3 @@ });

exports.get_key = function (connection) {
exports.get_concurrency_key = function (connection) {
return 'concurrency|' + connection.remote.ip;

@@ -204,3 +228,3 @@ };

var max = plugin.get_concurrency_limit(connection);
var max = plugin.get_limit('concurrency', connection);
if (!max || isNaN(max)) {

@@ -226,32 +250,2 @@ connection.results.add(plugin, {err: "concurrency: no limit?!"});

exports.get_concurrency_limit = function (connection) {
var plugin = this;
var history_plugin = plugin.cfg.concurrency.history;
if (!history_plugin) {
return plugin.cfg.concurrency.max;
}
var results = connection.results.get(history_plugin);
if (!results) {
connection.logerror(plugin, 'no ' + history_plugin + ' results,' +
' disabling history due to misconfiguration');
delete plugin.cfg.concurrency.history;
return plugin.cfg.concurrency.max;
}
if (results.history === undefined) {
connection.loginfo(plugin, 'no IP history from : ' + history_plugin);
return plugin.cfg.concurrency.max;
}
var history = parseFloat(results.history);
connection.logdebug(plugin, 'history: ' + history);
if (isNaN(history)) { history = 0; }
if (history < 0) { return plugin.cfg.concurrency.history_bad || 1; }
if (history > 0) { return plugin.cfg.concurrency.history_good || 5; }
return plugin.cfg.concurrency.history_none || 3;
};
exports.penalize = function (connection, disconnect, msg, next) {

@@ -274,9 +268,10 @@ var plugin = this;

exports.decr_concurrency = function (next, connection) {
exports.conn_concur_decr = function (next, connection) {
var plugin = this;
if (!plugin.db) return next();
if (!plugin.cfg.concurrency) return next();
var dbkey = plugin.get_key(connection);
var dbkey = plugin.get_concurrency_key(connection);
plugin.db.incrby(dbkey, -1, function (err, concurrent) {
if (err) connection.results.add(plugin, { err: 'decr_concurrency:' + err })
if (err) connection.results.add(plugin, { err: 'conn_concur_decr:' + err })
return next();

@@ -286,3 +281,3 @@ });

exports.lookup_host_key = function (type, remote, cb) {
exports.get_host_key = function (type, connection, cb) {
var plugin = this;

@@ -294,3 +289,3 @@ if (!plugin.cfg[type]) {

try {
var ip = ipaddr.parse(remote.ip);
var ip = ipaddr.parse(connection.remote.ip);
if (ip.kind === 'ipv6') {

@@ -317,4 +312,4 @@ ip = ipaddr.toNormalizedString();

// rDNS
if (remote.host) {
var rdns_array = remote.host.toLowerCase().split('.');
if (connection.remote.host) {
var rdns_array = connection.remote.host.toLowerCase().split('.');
while (rdns_array.length) {

@@ -329,2 +324,7 @@ var part2 = rdns_array.join('.');

if (plugin.cfg[type + '_history']) {
var history = plugin.get_history_limit(type, connection);
if (history) return cb(null, ip, history);
}
// Custom Default

@@ -402,4 +402,4 @@ if (plugin.cfg[type].default) {

var match = /^([\d]+)/.exec(value);
if (!match) return;
return match[1];
if (!match) return 0;
return parseInt(match[1], 10);
}

@@ -417,2 +417,3 @@

if (!key || !value) return cb();
if (!plugin.db) return cb();

@@ -432,12 +433,26 @@ var limit = getLimit(value);

if (newval === 1) plugin.db.expire(key, ttl);
cb(err, parseInt(newval) > parseInt(limit)); // boolean true/false
cb(err, parseInt(newval, 10) > limit); // boolean true/false
});
};
exports.rate_rcpt_host = function (next, connection) {
exports.rate_rcpt_host_incr = function (next, connection) {
var plugin = this;
if (!plugin.db) return next();
if (!plugin.cfg.rate_rcpt_host) return next();
plugin.get_host_key('rate_rcpt_host', connection, function (err, key, value) {
if (!key || !value) return next();
plugin.lookup_host_key('rate_rcpt_host', connection.remote, function (err, key, value) {
key = 'rate_rcpt_host:' + key;
plugin.db.incr(key, function (err2, newval) {
if (newval === 1) plugin.db.expire(key, getTTL(value));
next();
});
});
};
exports.rate_rcpt_host_enforce = function (next, connection) {
var plugin = this;
if (!plugin.db) return next();
plugin.get_host_key('rate_rcpt_host', connection, function (err, key, value) {
if (err) {

@@ -451,3 +466,3 @@ connection.results.add(plugin, { err: 'rate_rcpt_host:' + err });

var match = /^(\d+)/.exec(value);
var limit = match[0];
var limit = parseInt(match[0], 10);
if (!limit) return next();

@@ -463,8 +478,9 @@

connection.results.add(plugin, {
rate_rcpt_host: key + ':' + result + '/' + limit
rate_rcpt_host: key + ':' + result + ':' + value
});
if (result <= limit) return next();
connection.results.add(plugin, { fail: 'rate_rcpt_host' });
plugin.penalize(connection, false, 'connection rate limit exceeded', next);
plugin.penalize(connection, false, 'recipient rate limit exceeded', next);
});

@@ -474,6 +490,23 @@ });

exports.rate_conn = function (next, connection) {
exports.rate_conn_incr = function (next, connection) {
var plugin = this;
if (!plugin.db) return next();
plugin.lookup_host_key('rate_conn', connection.remote, function (err, key, value) {
plugin.get_host_key('rate_conn', connection, function (err, key, value) {
if (!key || !value) return next();
key = 'rate_conn:' + key;
plugin.db.hincrby(key, + new Date(), 1, function (err2, newval) {
// extend key expiration on every new connection
plugin.db.expire(key, getTTL(value) * 2);
next();
});
});
}
exports.rate_conn_enforce = function (next, connection) {
var plugin = this;
if (!plugin.db) return next();
plugin.get_host_key('rate_conn', connection, function (err, key, value) {
if (err) {

@@ -484,8 +517,6 @@ connection.results.add(plugin, { err: 'rate_conn:' + err });

if (value === 0) return next(); // limits disabled for host
if (!key || !value) return next();
var limit = getLimit(value);
var ttl = getTTL(value);
if (!limit || ! ttl) {
if (!limit) {
connection.results.add(plugin, { err: 'rate_conn:syntax:' + value });

@@ -495,3 +526,3 @@ return next();

plugin.db.incr('rate_conn:' + key, function (err2, newval) {
plugin.db.hgetall('rate_conn:' + key, function (err2, tstamps) {
if (err2) {

@@ -502,7 +533,14 @@ connection.results.add(plugin, { err: 'rate_conn:' + err });

if (newval === 1) plugin.db.expire('rate_conn:' + key, ttl);
var d = new Date();
d.setMinutes(d.getMinutes() - (getTTL(value) / 60));
var periodStartTs = + d; // date as integer
connection.results.add(plugin, { rate_conn: newval + '/' + limit});
var connections_in_ttl_period = 0;
Object.keys(tstamps).forEach(function (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});
if (parseInt(newval) <= parseInt(limit)) return next();
if (connections_in_ttl_period <= limit) return next();

@@ -592,8 +630,9 @@ connection.results.add(plugin, { fail: 'rate_conn' });

exports.increment_outbound_limit = function (next, hmail) {
exports.outbound_increment = function (next, hmail) {
var plugin = this;
if (!plugin.db) return next();
plugin.db.hincrby(getOutKey(hmail), 'TOTAL', 1, function (err, count) {
if (err) {
plugin.logerror("increment_outbound_limit: " + err);
plugin.logerror("outbound_increment: " + err);
return next(); // just deliver

@@ -614,5 +653,7 @@ }

exports.decrement_outbound_limit = function (next, hmail) {
this.db.hincrby(getOutKey(hmail), 'TOTAL', -1);
exports.outbound_decrement = function (next, hmail) {
var plugin = this;
if (!plugin.db) return next();
plugin.db.hincrby(getOutKey(hmail), 'TOTAL', -1);
return next();
}
}
{
"name": "haraka-plugin-limit",
"version": "1.0.0",
"version": "1.0.1",
"description": "enforce various types of limits on remote MTAs",

@@ -16,5 +16,6 @@ "main": "limit.js",

"devDependencies": {
"address-rfc2821": "^1.0.1",
"address-rfc2821": "*",
"eslint": "^3.14.1",
"haraka-test-fixtures": "^1.0.13",
"eslint-plugin-haraka": "*",
"haraka-test-fixtures": "*",
"nodeunit": "^0.10.2"

@@ -21,0 +22,0 @@ },

@@ -218,2 +218,12 @@ # limit

## CAUTION
Applying strict connection and rate limits is an effective way to reduce spam delivery. It's also an effective way to inflict a stampeding herd on your mail server. When spam/malware is delivered by MTAs that have queue retries, if you disconnect early (which the rate limits do) with a 400 series code (a sane default), the remote is likely to try again. And again. And again. And again. This can cause an obscene rise in the number of connections your mail server handles. Plan a strategy for handling that.
## Strategies
- Don't enforce limits early. I use karma and wait until DATA before disconnecting. By then, the score of the connection is determinate and I can return a 500 series code telling the remote not to try again.
- enforce rate limits with your firewall instead
### TODO

@@ -220,0 +230,0 @@

'use strict';
var path = require('path');
var fixtures = require('haraka-test-fixtures');

@@ -7,2 +8,3 @@

this.plugin = new fixtures.plugin('index');
this.plugin.config = this.plugin.config.module_config(path.resolve('test'));
done();

@@ -12,8 +14,9 @@ };

var default_config = {
main: {},
main: { tarpit_delay: 0 },
outbound: { enabled: false },
redis: { db: 4, host: '127.0.0.1', port: '6379' },
concurrency: { plugin: 'karma', good: 10, bad: 1, none: 2 },
recipients: {},
unrecognized_commands: {},
errors: {},
redis: { db: 4, host: '127.0.0.1', port: '6379' },
concurrency: { },
rate_conn: { '127': 0, default: 5 },

@@ -23,4 +26,3 @@ rate_rcpt_host: { '127': 0, default: '50/5m' },

rate_rcpt: { '127': 0, default: '50/5m' },
rate_rcpt_null: { default: 1 },
outbound: { enabled: false }
rate_rcpt_null: { default: 1 }
};

@@ -32,3 +34,3 @@

test.expect(1);
// gotta inhert b/c config loader merges in defaults from redis.ini
// gotta inherit b/c config loader merges in defaults from redis.ini
this.plugin.inherits('haraka-plugin-redis');

@@ -45,2 +47,3 @@ this.plugin.load_limit_ini();

},
};
'use strict';
var path = require('path');
var constants = require('haraka-constants');

@@ -9,7 +11,5 @@ var fixtures = require('haraka-test-fixtures');

this.plugin = new fixtures.plugin('index');
this.plugin.config = this.plugin.config.module_config(path.resolve('test'));
this.plugin.cfg = { main: {} };
this.connection = new fixtures.connection.createConnection();
this.connection.results = new fixtures.result_store(this.connection);
this.connection.transaction = new fixtures.transaction.createTransaction();

@@ -21,28 +21,2 @@

exports.inheritance = {
setUp : function (done) {
this.plugin = new fixtures.plugin('index');
done();
},
'inherits redis': function (test) {
test.expect(1);
this.plugin.inherits('haraka-plugin-redis');
test.equal(typeof this.plugin.load_redis_ini, 'function');
test.done();
},
'can call parent functions': function (test) {
test.expect(1);
this.plugin.inherits('haraka-plugin-redis');
this.plugin.load_redis_ini();
test.ok(this.plugin.redisCfg); // loaded config
test.done();
},
'register': function (test) {
test.expect(1);
this.plugin.register();
test.ok(this.plugin.cfg); // loaded config
test.done();
},
};
exports.max_errors = {

@@ -49,0 +23,0 @@ setUp : _set_up,

@@ -20,7 +20,7 @@ 'use strict';

exports.increment_outbound_limit = {
exports.outbound_increment = {
setUp : _set_up,
'no limit, no delay': function (test) {
test.expect(2);
this.plugin.increment_outbound_limit(function (code, msg) {
this.plugin.outbound_increment(function (code, msg) {
test.equal(code, undefined);

@@ -37,3 +37,3 @@ test.equal(msg, undefined);

self.plugin.db.hset('outbound-rate:slow.test.com', 'TOTAL', 4, function () {
self.plugin.increment_outbound_limit(function (code, delay) {
self.plugin.outbound_increment(function (code, delay) {
test.equal(code, constants.delay);

@@ -40,0 +40,0 @@ test.equal(delay, 30);

'use strict';
var path = require('path');
var Address = require('address-rfc2821').Address;

@@ -7,3 +9,3 @@ var constants = require('haraka-constants');

exports.lookup_host_key = {
exports.get_host_key = {
setUp : function (done) {

@@ -20,3 +22,3 @@ this.plugin = new fixtures.plugin('rate_limit');

test.expect(3);
this.plugin.lookup_host_key('rate_conn', this.connection.remote, function (err, ip, limit) {
this.plugin.get_host_key('rate_conn', this.connection, function (err, ip, limit) {
test.equal(err, undefined);

@@ -30,3 +32,3 @@ test.equal(ip, '1.2.3.4');

test.expect(3);
this.plugin.lookup_host_key('rate_rcpt_host', this.connection.remote, function (err, ip, limit) {
this.plugin.get_host_key('rate_rcpt_host', this.connection, function (err, ip, limit) {
test.equal(err, undefined);

@@ -110,2 +112,4 @@ test.equal(ip, '1.2.3.4');

this.plugin = new fixtures.plugin('rate_limit');
this.plugin.config = this.plugin.config.module_config(path.resolve('test'));
this.connection = new fixtures.connection.createConnection();

@@ -124,21 +128,49 @@ this.connection.remote.ip = '1.2.3.4';

test.expect(3);
this.plugin.rate_conn(function (code, msg) {
var plugin = this.plugin;
var connection = this.connection;
var rc = this.connection.results.get(this.plugin.name);
test.ok(rc.rate_conn);
plugin.rate_conn_incr(function () {
plugin.rate_conn_enforce(function (code, msg) {
var rc = connection.results.get(plugin.name);
test.ok(rc.rate_conn);
var match = /([\d]+)\/([\d]+)$/.exec(rc.rate_conn); // 1/5
var match = /([\d]+):(.*)$/.exec(rc.rate_conn); // 1/5
if (parseInt(match[1]) <= parseInt(match[2])) {
test.equal(code, undefined);
test.equal(msg, undefined);
}
else {
test.equal(code, constants.DENYSOFTDISCONNECT);
test.equal(msg, 'connection rate limit exceeded');
}
test.done();
}.bind(this),
this.connection);
if (parseInt(match[1]) <= parseInt(match[2])) {
test.equal(code, undefined);
test.equal(msg, undefined);
}
else {
test.equal(code, constants.DENYSOFTDISCONNECT);
test.equal(msg, 'connection rate limit exceeded');
}
test.done();
}.bind(this),
connection);
}, connection);
},
'defined limit' : function (test) {
test.expect(3);
var plugin = this.plugin;
var connection = this.connection;
plugin.cfg.rate_conn['1.2.3.4'] = '1/5m';
plugin.rate_conn_incr(function () {
plugin.rate_conn_enforce(function (code, msg) {
var rc = connection.results.get(plugin.name);
test.ok(rc.rate_conn);
var match = /^([\d]+):(.*)$/.exec(rc.rate_conn); // 1/5m
if (parseInt(match[1]) <= parseInt(match[2])) {
test.equal(code, undefined);
test.equal(msg, undefined);
}
else {
test.equal(code, constants.DENYSOFTDISCONNECT);
test.equal(msg, 'connection rate limit exceeded');
}
test.done();
}.bind(this),
connection);
}, connection);
},
}

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc