diff --git a/.codeclimate.yml b/.codeclimate.yml index 013b333..30364a2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,9 +1,9 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-8' config: - config: ".eslintrc.yaml" + config: '.eslintrc.yaml' checks: return-statements: @@ -12,8 +12,14 @@ checks: enabled: false method-complexity: config: - threshold: 10 + threshold: 15 + file-lines: + config: + threshold: 500 + method-lines: + config: + threshold: 50 ratings: - paths: - - "**.js" + paths: + - '**.js' diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 544da39..4c1bf2a 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -1,23 +1,10 @@ - env: node: true es6: true mocha: true - es2020: true - -plugins: [ haraka ] - -extends: [ eslint:recommended, plugin:haraka/recommended ] - -root: true + es2022: true -globals: - OK: true - CONT: true - DENY: true - DENYSOFT: true - DENYDISCONNECT: true - DENYSOFTDISCONNECT: true +extends: ['@haraka'] rules: - no-unused-vars: 1 \ No newline at end of file + no-unused-vars: 1 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 47e500a..662f77f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,9 +5,9 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" # Location of package manifests + - package-ecosystem: 'npm' + directory: '/' # Location of package manifests schedule: - interval: "weekly" + interval: 'weekly' allow: - - dependency-type: "production" \ No newline at end of file + - dependency-type: 'production' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2542a2..6395e0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,19 @@ name: CI -on: [ push, pull_request ] +on: [push, pull_request] env: CI: true jobs: - lint: uses: haraka/.github/.github/workflows/lint.yml@master ubuntu: - needs: [ lint ] + needs: [lint] uses: haraka/.github/.github/workflows/ubuntu.yml@master windows: - needs: [ lint ] + needs: [lint] uses: haraka/.github/.github/workflows/windows.yml@master - if: ${{ false }} # disabled, until Redis for GHA Windows exists + if: ${{ false }} # disabled, until Redis for GHA Windows exists diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3627451..8314a66 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: CodeQL on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - cron: '18 7 * * 4' diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 3e8e260..0000000 --- a/.npmignore +++ /dev/null @@ -1,58 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -package-lock.json -bower_components -# Optional npm cache directory -.npmrc -.idea -.DS_Store -haraka-update.sh - -.github -.release -.codeclimate.yml -.editorconfig -.gitignore -.gitmodules -.lgtm.yml -appveyor.yml -codecov.yml -.travis.yml -.eslintrc.yaml -.eslintrc.json diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..8ded5e0 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,2 @@ +singleQuote: true +semi: false diff --git a/.release b/.release index 0890e94..0fa4e69 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 0890e945e4e061c96c7b2ab45017525904c17728 +Subproject commit 0fa4e690ffabb0157e46d56f18e4f7cfe49ce291 diff --git a/Changes.md b/CHANGELOG.md similarity index 76% rename from Changes.md rename to CHANGELOG.md index f9471f1..4f4d2b2 100644 --- a/Changes.md +++ b/CHANGELOG.md @@ -1,29 +1,35 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [1.2.2] - 2024-04-22 + +- populate [files] in package.json. Delete .npmignore. +- dep: eslint-plugin-haraka -> @haraka/eslint-config +- lint: remove duplicate / stale rules from .eslintrc +- deps: bump versions +- format: prettier ### [1.2.1] - 2024-03-20 - deps: bump versions - fix: undo increment if the email sending is delayed (#56) - ### [1.2.0] - 2023-12-27 - disable history by default (match docs) - ci: use shared workflows - ### [1.1.1] - 2022-12-18 - package.json: remove deprecated 'main' - ### [1.1.0] - 2022-08-18 - initialize redis when only concurrency is enabled (#42) - ### [1.0.7] - 2022-06-03 - chore: add .release as a submodule @@ -31,48 +37,43 @@ - ci: populate test matrix with Node.js LTS versions - cfg: rename redis.db -> redis.database, pi-redis 2+ does this automatically, causing a test failure - ### 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 + 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 - for outbound, find domain at hmail.todo.domain then hmail.domain. - noop: use es6 arrow functions - ### 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 - when redis handle goes away, skip processing - add a 5 minute expiration on outbound rate limit entries - ### 1.0.1 - 2017-01-28 - increment rate_conn on connect_init - increment rate_rcpt_host on rcpt/rcpt_ok - +[1.0.6]: https://github.com/haraka/haraka-plugin-limit/releases/tag/1.0.6 [1.0.7]: https://github.com/haraka/haraka-plugin-limit/releases/tag/1.0.7 [1.1.0]: https://github.com/haraka/haraka-plugin-limit/releases/tag/1.1.0 -[1.1.1]: https://github.com/haraka/haraka-plugin-limit/releases/tag/1.1.1 -[1.2.0]: https://github.com/haraka/haraka-plugin-limit/releases/tag/1.2.0 -[1.2.1]: https://github.com/haraka/haraka-plugin-limit/releases/tag/1.2.1 +[1.1.1]: https://github.com/haraka/haraka-plugin-limit/releases/tag/v1.1.1 +[1.2.0]: https://github.com/haraka/haraka-plugin-limit/releases/tag/v1.2.0 +[1.2.1]: https://github.com/haraka/haraka-plugin-limit/releases/tag/v1.2.1 +[1.2.2]: https://github.com/haraka/haraka-plugin-limit/releases/tag/v1.2.2 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..ff90340 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +# Contributors + +This handcrafted artisinal software is brought to you by: + +|
msimerson (68) |
divine (6) |
gramakri (2) |
leadbi (1) | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | + +this file is maintained by [.release](https://github.com/msimerson/.release) diff --git a/README.md b/README.md index 987a5ca..25bd716 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ Apply many types of limits to SMTP connections: - by sender - max null recipients / period - ## Installation ```sh @@ -37,67 +36,58 @@ Each limit type is disabled until `enabled=true` is set within its block in limi Haraka's config loader loads the defaults from limit.ini within this plugins installed config directory and applies any overrides found in the limit.ini within your Haraka install/config directory. - ### [main] -- tarpit_delay = seconds *(optional)* + +- tarpit_delay = seconds _(optional)_ Set this to the length in seconds that you want to delay every SMTP response to a remote client that has exceeded the rate limits. - ## [redis] Redis is the cluster-safe storage backend for maintaining the counters necessary to impose limits reliably. - host (default: 127.0.0.1) - port (default: 6379) -- db (default: 0) +- db (default: 0) If this [redis] section or any values are missing, the defaults from redis.ini are used. - ## concurrency When `[concurrency]max` is defined, it limits the maximum number of simultaneous connections per IP address. Connection attempts in excess of the limit are optionally delayed before being disconnected. This works well in conjunction with a history / reputation database, so that one can assign very low concurrency (1) to bad or unknown senders and higher limits for reputable mail servers. - ### History History: when enabled, the `history` setting is the name of a plugin that stores IP history / reputation results. The result store must have a positive value for good connections and negative integers for poor / undesirable connections. Karma is one such plugin. - ## recipients When `[recipients]max` is defined, each connection is limited to that number of recipients. The limit is imposed against **all** recipient attempts. Attempts in excess of the limit are issued a temporary failure. - ## unrecognized_commands When `[unrecognized_commands]max` is set, a connection that exceeeds the limit is disconnected. Unrecognized commands are normally SMTP verbs invalidly issued by the client. Examples: -* issuing AUTH when we didn't advertise AUTH extension -* issuing STARTTLS when we didn't advertise STARTTLS -* invalid SMTP verbs - +- issuing AUTH when we didn't advertise AUTH extension +- issuing STARTTLS when we didn't advertise STARTTLS +- invalid SMTP verbs ### Limitations The unrecognized_command hook is used by the `tls` and `auth` plugins, so running this plugin before those would result in valid operations getting counted against that connections limits. The solution is simple: list `limit` in config/plugins after those. - ## errors When `[errors]max` is set, a connection that exceeeds the limit is disconnected. Errors that count against this limit include: -* issuing commands out of turn (MAIL before EHLO, RCPT before MAIL, etc) -* attempting MAIL on port 465/587 without AUTH -* MAIL or RCPT addresses that fail to parse - - +- issuing commands out of turn (MAIL before EHLO, RCPT before MAIL, etc) +- attempting MAIL on port 465/587 without AUTH +- MAIL or RCPT addresses that fail to parse # Rate Limits @@ -108,7 +98,7 @@ Missing sections disable that particular test. They all use a common configuration format: -- \ = \[/time[unit]] *(optional)* +- \ = \[/time[unit]] _(optional)_ 'lookup' is based upon the limit being enforced and is either an IP address, rDNS name, sender address or recipient address either in full or part. @@ -116,7 +106,7 @@ The lookup order is as follows and the first match in this order is returned and **IPv4/IPv6 address or rDNS hostname:** -```` +``` fe80:0:0:0:202:b3ff:fe1e:8329 fe80:0:0:0:202:b3ff:fe1e fe80:0:0:0:202:b3ff @@ -134,7 +124,7 @@ The lookup order is as follows and the first match in this order is returned and domain.com com default -```` +``` **Sender or Recipient address:** @@ -146,19 +136,18 @@ The lookup order is as follows and the first match in this order is returned and domain.com com default -```` +``` In all tests 'default' is used to specify a default limit if nothing else has matched. -'limit' specifies the limit for this lookup. Specify 0 (zero) to disable limits on a matching lookup. +'limit' specifies the limit for this lookup. Specify 0 (zero) to disable limits on a matching lookup. 'time' is optional and if missing defaults to 60 seconds. You can optionally specify the following time units (case-insensitive): - - s (seconds) - - m (minutes) - - h (hours) - - d (days) - +- s (seconds) +- m (minutes) +- h (hours) +- d (days) ### [rate_conn] @@ -166,35 +155,30 @@ This section limits the number of connections per interval from a given host or IP and rDNS names are looked up by this test. - ### [rate_rcpt_host] This section limits the number of recipients per interval from a given host or set of hosts. IP and rDNS names are looked up by this test. - ### [rate_rcpt_sender] This section limits the number of recipients per interval from a sender or sender domain. The sender is looked up by this test. - ### [rate_rcpt] This section limits the rate which a recipient or recipient domain can receive messages over an interval. Each recipient is looked up by this test. - ### [rate_rcpt_null] This section limits the rate at which a recipient can receive messages from a null sender (e.g. DSN, MDN etc.) over an interval. Each recipient is looked up by this test. - ### [outbound] enabled=true @@ -205,7 +189,6 @@ The number after the domain is the maximum concurrency limit for that domain. Delay is the number of seconds to wait before retrying this message. Outbound concurrency is checked on every attempt to deliver. - ## 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. @@ -215,7 +198,6 @@ Applying strict connection and rate limits is an effective way to reduce spam de - 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 - [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 diff --git a/index.js b/index.js index 5b58aae..ba6fa7a 100644 --- a/index.js +++ b/index.js @@ -1,581 +1,581 @@ -'use strict'; - -const constants = require('haraka-constants'); -const ipaddr = require('ipaddr.js'); +const constants = require('haraka-constants') +const ipaddr = require('ipaddr.js') exports.register = function () { - this.inherits('haraka-plugin-redis'); - - this.load_limit_ini(); - let needs_redis = 0 - - if (this.cfg.concurrency.enabled) { - needs_redis++; - this.register_hook('connect_init', 'conn_concur_incr'); - this.register_hook('connect', 'check_concurrency'); - this.register_hook('disconnect', 'conn_concur_decr'); - } - - if (this.cfg.errors.enabled) { - ['helo','ehlo','mail','rcpt','data'].forEach(hook => { - this.register_hook(hook, 'max_errors'); - }); - } - - if (this.cfg.recipients.enabled) { - this.register_hook('rcpt', 'max_recipients'); - } - - if (this.cfg.unrecognized_commands.enabled) { - this.register_hook('unrecognized_command', 'max_unrecognized_commands'); - } - - if (this.cfg.rate_conn.enabled) { - needs_redis++; - this.register_hook('connect_init', 'rate_conn_incr'); - this.register_hook('connect', 'rate_conn_enforce'); - } - 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 (this.cfg.rate_rcpt_sender.enabled) { - needs_redis++; - this.register_hook('rcpt', 'rate_rcpt_sender'); - } - if (this.cfg.rate_rcpt_null.enabled) { - needs_redis++; - this.register_hook('rcpt', 'rate_rcpt_null'); - } - if (this.cfg.rate_rcpt.enabled) { - needs_redis++; - this.register_hook('rcpt', 'rate_rcpt'); - } - - 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'); - } + this.inherits('haraka-plugin-redis') + + this.load_limit_ini() + let needs_redis = 0 + + if (this.cfg.concurrency.enabled) { + needs_redis++ + this.register_hook('connect_init', 'conn_concur_incr') + this.register_hook('connect', 'check_concurrency') + this.register_hook('disconnect', 'conn_concur_decr') + } + + if (this.cfg.errors.enabled) { + for (const hook of ['helo', 'ehlo', 'mail', 'rcpt', 'data']) { + this.register_hook(hook, 'max_errors') + } + } + + if (this.cfg.recipients.enabled) { + this.register_hook('rcpt', 'max_recipients') + } + + if (this.cfg.unrecognized_commands.enabled) { + this.register_hook('unrecognized_command', 'max_unrecognized_commands') + } + + if (this.cfg.rate_conn.enabled) { + needs_redis++ + this.register_hook('connect_init', 'rate_conn_incr') + this.register_hook('connect', 'rate_conn_enforce') + } + 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 (this.cfg.rate_rcpt_sender.enabled) { + needs_redis++ + this.register_hook('rcpt', 'rate_rcpt_sender') + } + if (this.cfg.rate_rcpt_null.enabled) { + needs_redis++ + this.register_hook('rcpt', 'rate_rcpt_null') + } + if (this.cfg.rate_rcpt.enabled) { + needs_redis++ + this.register_hook('rcpt', 'rate_rcpt') + } + + 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') + } } exports.load_limit_ini = function () { - this.cfg = this.config.get('limit.ini', { - booleans: [ - '-outbound.enabled', - '-recipients.enabled', - '-unrecognized_commands.enabled', - '-errors.enabled', - '-rate_conn.enabled', - '-rate_rcpt.enabled', - '-rate_rcpt_host.enabled', - '-rate_rcpt_sender.enabled', - '-rate_rcpt_null.enabled', - '-concurrency_history.enabled', - '-recipients_history.enabled', - '-rate_conn_history.enabled' - ] + this.cfg = this.config.get( + 'limit.ini', + { + booleans: [ + '-outbound.enabled', + '-recipients.enabled', + '-unrecognized_commands.enabled', + '-errors.enabled', + '-rate_conn.enabled', + '-rate_rcpt.enabled', + '-rate_rcpt_host.enabled', + '-rate_rcpt_sender.enabled', + '-rate_rcpt_null.enabled', + '-concurrency_history.enabled', + '-recipients_history.enabled', + '-rate_conn_history.enabled', + ], }, () => { - this.load_limit_ini(); - }); + this.load_limit_ini() + }, + ) - if (!this.cfg.concurrency) { // no config file - this.cfg.concurrency = {}; - } + if (!this.cfg.concurrency) { + // no config file + this.cfg.concurrency = {} + } - this.merge_redis_ini(); + this.merge_redis_ini() } exports.shutdown = function () { - if (this.db) this.db.quit(); + if (this.db) this.db.quit() } exports.max_unrecognized_commands = function (next, connection, cmd) { + if (!this.cfg.unrecognized_commands) return next() - if (!this.cfg.unrecognized_commands) return next(); - - connection.results.push(this, {unrec_cmds: cmd, emit: true}); + connection.results.push(this, { unrec_cmds: cmd, emit: true }) - const max = parseFloat(this.cfg.unrecognized_commands.max); - if (!max || isNaN(max)) return next(); + const max = parseFloat(this.cfg.unrecognized_commands.max) + if (!max || isNaN(max)) return next() - const uc = connection.results.get(this).unrec_cmds; - if (!uc || !uc.length) return next(); + const uc = connection.results.get(this).unrec_cmds + if (!uc || !uc.length) return next() - if (uc.length <= max) return next(); + if (uc.length <= max) return next() - connection.results.add(this, { fail: 'unrec_cmds.max' }); - this.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) { - if (!this.cfg.errors) return next(); // disabled in config + if (!this.cfg.errors) return next() // disabled in config - const max = parseFloat(this.cfg.errors.max); - if (!max || isNaN(max)) return next(); + const max = parseFloat(this.cfg.errors.max) + if (!max || isNaN(max)) return next() - if (connection.errors <= max) return next(); + if (connection.errors <= max) return next() - connection.results.add(this, {fail: 'errors.max'}); - this.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) { - if (!this.cfg.recipients) return next(); // disabled in config + if (!this.cfg.recipients) return next() // disabled in config - const max = this.get_limit('recipients', connection); - if (!max || isNaN(max)) return next(); + const max = this.get_limit('recipients', connection) + if (!max || isNaN(max)) return next() - const c = connection.rcpt_count; - const count = c.accept + c.tempfail + c.reject + 1; - if (count <= max) return next(); + const c = connection.rcpt_count + const count = c.accept + c.tempfail + c.reject + 1 + if (count <= max) return next() - connection.results.add(this, { fail: 'recipients.max' }); - this.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 history_cfg = `${type}_history`; - if (!this.cfg[history_cfg] || !this.cfg[history_cfg].enabled) return; - - const history_plugin = this.cfg[history_cfg].plugin; - if (!history_plugin) return; - - const results = connection.results.get(history_plugin); - if (!results) { - connection.logerror(this, `no ${history_plugin} results, disabling history due to misconfiguration`); - delete this.cfg[history_cfg]; - return; - } - - if (results.history === undefined) { - connection.logdebug(this, `no history from : ${history_plugin}`); - return; - } - - const history = parseFloat(results.history); - connection.logdebug(this, `history: ${history}`); - if (isNaN(history)) return; - - if (history > 0) return this.cfg[history_cfg].good; - if (history < 0) return this.cfg[history_cfg].bad; - return this.cfg[history_cfg].none; + const history_cfg = `${type}_history` + if (!this.cfg[history_cfg] || !this.cfg[history_cfg].enabled) return + + const history_plugin = this.cfg[history_cfg].plugin + if (!history_plugin) return + + const results = connection.results.get(history_plugin) + if (!results) { + connection.logerror( + this, + `no ${history_plugin} results, disabling history due to misconfiguration`, + ) + delete this.cfg[history_cfg] + return + } + + if (results.history === undefined) { + connection.logdebug(this, `no history from : ${history_plugin}`) + return + } + + const history = parseFloat(results.history) + connection.logdebug(this, `history: ${history}`) + if (isNaN(history)) return + + 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) { - - if (type === 'recipients') { - if (connection.relaying && this.cfg.recipients.max_relaying) { - return this.cfg.recipients.max_relaying; - } + if (type === 'recipients') { + if (connection.relaying && this.cfg.recipients.max_relaying) { + return this.cfg.recipients.max_relaying } + } - if (this.cfg[`${type}_history`]) { - const history = this.get_history_limit(type, connection); - if (history) return history; - } + if (this.cfg[`${type}_history`]) { + const history = this.get_history_limit(type, connection) + if (history) return history + } - return this.cfg[type].max || this.cfg[type].default; + return this.cfg[type].max || this.cfg[type].default } exports.conn_concur_incr = async function (next, connection) { - if (!this.db) return next(); - if (!this.cfg.concurrency) return next(); + if (!this.db) return next() + if (!this.cfg.concurrency) return next() - const dbkey = this.get_concurrency_key(connection); + const dbkey = this.get_concurrency_key(connection) - try { - const count = await this.db.incr(dbkey) + try { + const count = await this.db.incr(dbkey) - if (isNaN(count)) { - connection.results.add(this, {err: 'conn_concur_incr got isNaN'}); - return next(); - } - - connection.results.add(this, { concurrent_count: count }); + if (isNaN(count)) { + connection.results.add(this, { err: 'conn_concur_incr got isNaN' }) + return next() + } - // repair negative concurrency counters - if (count < 1) { - connection.results.add(this, { - msg: `resetting concurrent ${count} to 1` - }); - this.db.set(dbkey, 1); - } + connection.results.add(this, { concurrent_count: count }) - this.db.expire(dbkey, 3 * 60); // 3 minute lifetime + // repair negative concurrency counters + if (count < 1) { + connection.results.add(this, { + msg: `resetting concurrent ${count} to 1`, + }) + this.db.set(dbkey, 1) } - catch (err) { - connection.results.add(this, { err: `conn_concur_incr:${err}` }); - } - next(); + + this.db.expire(dbkey, 3 * 60) // 3 minute lifetime + } catch (err) { + connection.results.add(this, { err: `conn_concur_incr:${err}` }) + } + next() } exports.get_concurrency_key = function (connection) { - return `concurrency|${connection.remote.ip}`; + return `concurrency|${connection.remote.ip}` } exports.check_concurrency = function (next, connection) { + const max = this.get_limit('concurrency', connection) + if (!max || isNaN(max)) { + connection.results.add(this, { err: 'concurrency: no limit?!' }) + return next() + } - const max = this.get_limit('concurrency', connection); - if (!max || isNaN(max)) { - connection.results.add(this, {err: "concurrency: no limit?!"}); - return next(); - } + const count = parseInt(connection.results.get(this.name).concurrent_count) + if (isNaN(count)) { + connection.results.add(this, { err: 'concurrent.unset' }) + return next() + } - const count = parseInt(connection.results.get(this.name).concurrent_count); - if (isNaN(count)) { - connection.results.add(this, { err: 'concurrent.unset' }); - return next(); - } - - connection.results.add(this, { concurrent: `${count}/${max}` }); + connection.results.add(this, { concurrent: `${count}/${max}` }) - if (count <= max) return next(); + if (count <= max) return next() - connection.results.add(this, { fail: 'concurrency.max' }); + connection.results.add(this, { fail: 'concurrency.max' }) - this.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 code = disconnect ? constants.DENYSOFTDISCONNECT : constants.DENYSOFT; + const code = disconnect ? constants.DENYSOFTDISCONNECT : constants.DENYSOFT - if (!this.cfg.main.tarpit_delay) return next(code, msg); + if (!this.cfg.main.tarpit_delay) return next(code, msg) - const delay = this.cfg.main.tarpit_delay; - connection.loginfo(this, `tarpitting for ${delay}s`); + const delay = this.cfg.main.tarpit_delay + connection.loginfo(this, `tarpitting for ${delay}s`) - setTimeout(() => { - if (!connection) return; - next(code, msg); - }, delay * 1000); + setTimeout(() => { + if (!connection) return + next(code, msg) + }, delay * 1000) } exports.conn_concur_decr = async function (next, connection) { - - 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(); + 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) { - - if (!this.cfg[type]) { - connection.results.add(this, { err: `${type}: not configured` }); - return - } - - let ip; - try { - ip = ipaddr.parse(connection.remote.ip); - if (ip.kind === 'ipv6') { - ip = ipaddr.toNormalizedString(); - } - else { - ip = ip.toString(); - } - } - catch (err) { - connection.results.add(this, { err: `${type}: ${err.message}` }); - return - } - - const ip_array = ((ip.kind === 'ipv6') ? ip.split(':') : ip.split('.')); - while (ip_array.length) { - const part = ((ip.kind === 'ipv6') ? ip_array.join(':') : ip_array.join('.')); - if (this.cfg[type][part] || this.cfg[type][part] === 0) { - return [ part, this.cfg[type][part] ] - } - ip_array.pop(); - } - - // rDNS - if (connection.remote.host) { - const rdns_array = connection.remote.host.toLowerCase().split('.'); - while (rdns_array.length) { - const part2 = rdns_array.join('.'); - if (this.cfg[type][part2] || this.cfg[type][part2] === 0) { - return [ part2, this.cfg[type][part2] ] - } - rdns_array.pop(); - } - } - - if (this.cfg[`${type}_history`]) { - const history = this.get_history_limit(type, connection); - if (history) return [ ip, history ] - } - - // Custom Default - if (this.cfg[type].default) { - return [ ip, this.cfg[type].default ] - } - - // Default 0 = unlimited - return [ ip, 0 ] + if (!this.cfg[type]) { + connection.results.add(this, { err: `${type}: not configured` }) + return + } + + let ip + try { + ip = ipaddr.parse(connection.remote.ip) + if (ip.kind === 'ipv6') { + ip = ipaddr.toNormalizedString() + } else { + ip = ip.toString() + } + } catch (err) { + connection.results.add(this, { err: `${type}: ${err.message}` }) + return + } + + const ip_array = ip.kind === 'ipv6' ? ip.split(':') : ip.split('.') + while (ip_array.length) { + const part = ip.kind === 'ipv6' ? ip_array.join(':') : ip_array.join('.') + if (this.cfg[type][part] || this.cfg[type][part] === 0) { + return [part, this.cfg[type][part]] + } + ip_array.pop() + } + + // rDNS + if (connection.remote.host) { + const rdns_array = connection.remote.host.toLowerCase().split('.') + while (rdns_array.length) { + const part2 = rdns_array.join('.') + if (this.cfg[type][part2] || this.cfg[type][part2] === 0) { + return [part2, this.cfg[type][part2]] + } + rdns_array.pop() + } + } + + if (this.cfg[`${type}_history`]) { + const history = this.get_history_limit(type, connection) + if (history) return [ip, history] + } + + // Custom Default + if (this.cfg[type].default) { + return [ip, this.cfg[type].default] + } + + // Default 0 = unlimited + return [ip, 0] } 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 (this.cfg[type][email] || this.cfg[type][email] === 0) { - return [ email, this.cfg[type][email] ] - } - - // RHS parts e.g. host.sub.sub.domain.com - if (mail.host) { - const rhs_split = mail.host.toLowerCase().split('.'); - while (rhs_split.length) { - const part = rhs_split.join('.'); - if (this.cfg[type][part] || this.cfg[type][part] === 0) { - return [ part, this.cfg[type][part] ] - } - rhs_split.pop(); - } - } - - // Custom Default - if (this.cfg[type].default) { - return [ email, this.cfg[type].default ] - } - - // Default 0 = unlimited - return [ email, 0 ] + if (!this.cfg[type] || !mail) return + + // Full e-mail address (e.g. smf@fsl.com) + const email = mail.address() + if (this.cfg[type][email] || this.cfg[type][email] === 0) { + return [email, this.cfg[type][email]] + } + + // RHS parts e.g. host.sub.sub.domain.com + if (mail.host) { + const rhs_split = mail.host.toLowerCase().split('.') + while (rhs_split.length) { + const part = rhs_split.join('.') + if (this.cfg[type][part] || this.cfg[type][part] === 0) { + return [part, this.cfg[type][part]] + } + rhs_split.pop() + } + } + + // Custom Default + if (this.cfg[type].default) { + return [email, this.cfg[type].default] + } + + // Default 0 = unlimited + return [email, 0] } -function getTTL (value) { - - const match = /^(\d+)(?:\/(\d+)(\S)?)?$/.exec(value); - if (!match) return; - - const qty = match[2]; - const units = match[3]; - - let ttl = qty ? qty : 60; // Default 60s - if (!units) return ttl; - - // Unit - switch (units.toLowerCase()) { - case 's': // Default is seconds - break; - case 'm': - ttl *= 60; // minutes - break; - case 'h': - ttl *= (60*60); // hours - break; - case 'd': - ttl *= (60*60*24); // days - break; - default: - return ttl; - } - return ttl; +function getTTL(value) { + const match = /^(\d+)(?:\/(\d+)(\S)?)?$/.exec(value) + if (!match) return + + const qty = match[2] + const units = match[3] + + let ttl = qty ? qty : 60 // Default 60s + if (!units) return ttl + + // Unit + switch (units.toLowerCase()) { + case 's': // Default is seconds + break + case 'm': + ttl *= 60 // minutes + break + case 'h': + ttl *= 60 * 60 // hours + break + case 'd': + ttl *= 60 * 60 * 24 // days + break + default: + return ttl + } + return ttl } -function getLimit (value) { - const match = /^([\d]+)/.exec(value); - if (!match) return 0; - return parseInt(match[1], 10); +function getLimit(value) { + const match = /^([\d]+)/.exec(value) + if (!match) return 0 + return parseInt(match[1], 10) } 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 false - } - - // CAUTION: !value would match that 0 value -^ - if (!key || !value) return - if (!this.db) return - - const limit = getLimit(value); - const ttl = getTTL(value); - - if (!limit || ! ttl) { - connection.results.add(this, { err: `syntax error: key=${key} value=${value}` }); - return - } - - connection.logdebug(this, `key=${key} limit=${limit} ttl=${ttl}`); - - 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}` }); - } + if (value === 0) { + // Limit disabled for this host + connection.loginfo(this, `rate limit disabled for: ${key}`) + return false + } + + // CAUTION: !value would match that 0 value -^ + if (!key || !value) return + if (!this.db) return + + const limit = getLimit(value) + const ttl = getTTL(value) + + if (!limit || !ttl) { + connection.results.add(this, { + err: `syntax error: key=${key} value=${value}`, + }) + return + } + + connection.logdebug(this, `key=${key} limit=${limit} ttl=${ttl}`) + + 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 = async function (next, connection) { - if (!this.db) return next(); - - const [ key, value ] = this.get_host_key('rate_rcpt_host', connection) - if (!key || !value) return 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(); + if (!this.db) return next() + + const [key, value] = this.get_host_key('rate_rcpt_host', connection) + if (!key || !value) return 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 = async function (next, connection) { - if (!this.db) return next(); + if (!this.db) return next() - const [ key, value ] = this.get_host_key('rate_rcpt_host', connection) - if (!key || !value) return next(); + const [key, value] = this.get_host_key('rate_rcpt_host', connection) + 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}`) + try { + const result = await this.db.get(`rate_rcpt_host:${key}`) - if (!result) return next(); - connection.results.add(this, { - rate_rcpt_host: `${key}:${result}:${value}` - }); + if (!result) return next() + connection.results.add(this, { + rate_rcpt_host: `${key}:${result}:${value}`, + }) - if (result <= limit) return next(); + if (result <= limit) return 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(); - } + 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 = async function (next, connection) { - if (!this.db) return next(); - - const [ key, value ] = this.get_host_key('rate_conn', connection) - if (!key || !value) return 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() + if (!this.db) return next() + + const [key, value] = this.get_host_key('rate_conn', connection) + if (!key || !value) return 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 = async function (next, connection) { - if (!this.db) return next(); + if (!this.db) return next() - const [ key, value ] = this.get_host_key('rate_conn', connection) - if (!key || !value) return next(); + const [key, value] = this.get_host_key('rate_conn', connection) + 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(this, { err: `rate_conn:syntax:${value}` }) + return next() + } - try { - const tstamps = await this.db.hGetAll(`rate_conn:${key}`) - if (!tstamps) { - connection.results.add(this, { err: 'rate_conn:no_tstamps' }); - return next(); - } + try { + const tstamps = await this.db.hGetAll(`rate_conn:${key}`) + if (!tstamps) { + connection.results.add(this, { err: 'rate_conn:no_tstamps' }) + return next() + } - const d = new Date(); - d.setMinutes(d.getMinutes() - (getTTL(value) / 60)); - const periodStartTs = + d; // date as integer + const d = new Date() + d.setMinutes(d.getMinutes() - getTTL(value) / 60) + const periodStartTs = +d // date as integer - 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}`}); + let connections_in_ttl_period = 0 + for (const ts of Object.keys(tstamps)) { + 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}`, + }) - if (connections_in_ttl_period <= limit) return next(); + if (connections_in_ttl_period <= limit) return next() - connection.results.add(this, { fail: 'rate_conn' }); + connection.results.add(this, { fail: 'rate_conn' }) - this.penalize(connection, true, 'connection rate limit exceeded', next); - } - catch (err) { - connection.results.add(this, { err: `rate_conn:${err}` }); - 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 = async function (next, connection, params) { - - const [ key, value ] = this.get_mail_key('rate_rcpt_sender', connection.transaction.mail_from) - connection.results.add(this, { rate_rcpt_sender: value }); - - const over = await this.rate_limit(connection, `rate_rcpt_sender:${key}`, value) - if (!over) return next(); - - connection.results.add(this, { fail: 'rate_rcpt_sender' }); - this.penalize(connection, false, 'rcpt rate limit exceeded', next); + const [key, value] = this.get_mail_key( + 'rate_rcpt_sender', + connection.transaction.mail_from, + ) + connection.results.add(this, { rate_rcpt_sender: value }) + + const over = await this.rate_limit( + connection, + `rate_rcpt_sender:${key}`, + value, + ) + if (!over) return next() + + connection.results.add(this, { fail: 'rate_rcpt_sender' }) + this.penalize(connection, false, 'rcpt rate limit exceeded', next) } exports.rate_rcpt_null = async function (next, connection, params) { + if (!params) return next() + if (Array.isArray(params)) params = params[0] + if (params.user) return next() - if (!params) return next(); - if (Array.isArray(params)) params = params[0]; - if (params.user) return next(); + // Message from the null sender + const [key, value] = this.get_mail_key('rate_rcpt_null', params) + connection.results.add(this, { rate_rcpt_null: value }) - // Message from the null sender - const [ key, value ] = this.get_mail_key('rate_rcpt_null', params) - connection.results.add(this, { rate_rcpt_null: value }); + const over = await this.rate_limit(connection, `rate_rcpt_null:${key}`, value) + if (!over) return next() - const over = await this.rate_limit(connection, `rate_rcpt_null:${key}`, value) - if (!over) return next(); - - connection.results.add(this, { fail: 'rate_rcpt_null' }); - this.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 = async function (next, connection, params) { - const plugin = this; - if (Array.isArray(params)) params = params[0]; + const plugin = this + if (Array.isArray(params)) params = params[0] - const [ key, value ] = plugin.get_mail_key('rate_rcpt', params) - connection.results.add(plugin, { rate_rcpt: value }); + const [key, value] = plugin.get_mail_key('rate_rcpt', params) + connection.results.add(plugin, { rate_rcpt: value }) - const over = await plugin.rate_limit(connection, `rate_rcpt:${key}`, value) - if (!over) return next(); + const over = await plugin.rate_limit(connection, `rate_rcpt:${key}`, value) + 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) } /* @@ -583,47 +583,46 @@ exports.rate_rcpt = async function (next, connection, params) { * */ -function getOutDom (hmail) { - // outbound isn't internally consistent using hmail.domain and hmail.todo.domain. - // TODO: fix haraka/Haraka/outbound/HMailItem to be internally consistent. - return hmail?.todo?.domain || hmail.domain; +function getOutDom(hmail) { + // outbound isn't internally consistent using hmail.domain and hmail.todo.domain. + // TODO: fix haraka/Haraka/outbound/HMailItem to be internally consistent. + return hmail?.todo?.domain || hmail.domain } -function getOutKey (domain) { - return `outbound-rate:${domain}`; +function getOutKey(domain) { + return `outbound-rate:${domain}` } exports.outbound_increment = async function (next, hmail) { - if (!this.db) return next(); + if (!this.db) return next() - const outDom = getOutDom(hmail); - const outKey = getOutKey(outDom); + const outDom = getOutDom(hmail) + const outKey = getOutKey(outDom) - try { - let count = await this.db.hIncrBy(outKey, 'TOTAL', 1) + try { + let count = await this.db.hIncrBy(outKey, 'TOTAL', 1) - this.db.expire(outKey, 300); // 5 min expire + this.db.expire(outKey, 300) // 5 min expire - if (!this.cfg.outbound[outDom]) return next(); - const limit = parseInt(this.cfg.outbound[outDom], 10); - if (!limit) return next(); + if (!this.cfg.outbound[outDom]) return next() + const limit = parseInt(this.cfg.outbound[outDom], 10) + if (!limit) return next() - count = parseInt(count, 10); - if (count <= limit) return next(); + count = parseInt(count, 10) + if (count <= limit) return next() - this.db.hIncrBy(outKey, 'TOTAL', -1); // undo the increment - const delay = this.cfg.outbound.delay || 30; - next(constants.delay, delay); - } - catch (err) { - this.logerror(`outbound_increment: ${err}`); - next(); // just deliver - } + this.db.hIncrBy(outKey, 'TOTAL', -1) // undo the increment + 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) { - if (!this.db) return next(); + if (!this.db) return next() - this.db.hIncrBy(getOutKey(getOutDom(hmail)), 'TOTAL', -1); - next(); + this.db.hIncrBy(getOutKey(getOutDom(hmail)), 'TOTAL', -1) + next() } diff --git a/package.json b/package.json index f1e76d9..4dc683d 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,33 @@ { "name": "haraka-plugin-limit", - "version": "1.2.1", + "version": "1.2.2", "description": "enforce various types of limits on remote MTAs", + "files": [ + "config" + ], "directories": { "test": "test" }, "dependencies": { - "haraka-constants": "1.0.6", - "haraka-plugin-redis": "^2.0.6", - "ipaddr.js": "^2.1.0", + "haraka-constants": "^1.0.6", + "haraka-plugin-redis": "^2.0.7", + "ipaddr.js": "^2.2.0", "redis": "^4.6.13" }, "devDependencies": { - "address-rfc2821": "2.1.2", - "eslint-plugin-haraka": "1.0.15", - "haraka-test-fixtures": "1.3.3", - "mocha": "^10.3.0" + "address-rfc2821": "^2.1.2", + "@haraka/eslint-config": "^1.1.5", + "haraka-test-fixtures": "^1.3.7" }, "scripts": { - "lint": "npx eslint *.js test", - "lintfix": "npx eslint --fix *.js test", + "format": "npm run prettier:fix && npm run lint:fix", + "lint": "npx eslint@^8 *.js test", + "lint:fix": "npx eslint@^8 *.js test --fix", + "prettier": "npx prettier . --check", + "prettier:fix": "npx prettier . --write --log-level=warn", + "test": "npx mocha@10 --exit", "versions": "npx dependency-version-checker check", - "test": "npx mocha --exit" + "versions:fix": "npx dependency-version-checker update && npm run prettier:fix" }, "repository": { "type": "git", diff --git a/test/config.js b/test/config.js index abe465e..5ef310d 100644 --- a/test/config.js +++ b/test/config.js @@ -1,44 +1,42 @@ -'use strict'; const assert = require('assert') -const path = require('path'); -const fixtures = require('haraka-test-fixtures'); +const path = require('path') +const fixtures = require('haraka-test-fixtures') const default_config = { - main: { tarpit_delay: 0 }, - outbound: { enabled: false }, - recipients: { enabled: false }, - recipients_history: { enabled: false }, - unrecognized_commands: { enabled: false }, - errors: { enabled: false }, - rate_conn: { '127': 0, enabled: false, default: 5 }, - rate_conn_history: { enabled: false }, - rate_rcpt: { '127': 0, enabled: false, default: '50/5m' }, - rate_rcpt_host: { '127': 0, enabled: false, default: '50/5m' }, - rate_rcpt_sender: { '127': 0, enabled: false, default: '50/5m' }, - rate_rcpt_null: { enabled: false, default: 1 }, - redis: { database: 4, socket: { host: '127.0.0.1', port: '6379' } }, - concurrency: { plugin: 'karma', good: 10, bad: 1, none: 2 }, - concurrency_history: { enabled: false }, -}; + main: { tarpit_delay: 0 }, + outbound: { enabled: false }, + recipients: { enabled: false }, + recipients_history: { enabled: false }, + unrecognized_commands: { enabled: false }, + errors: { enabled: false }, + rate_conn: { 127: 0, enabled: false, default: 5 }, + rate_conn_history: { enabled: false }, + rate_rcpt: { 127: 0, enabled: false, default: '50/5m' }, + rate_rcpt_host: { 127: 0, enabled: false, default: '50/5m' }, + rate_rcpt_sender: { 127: 0, enabled: false, default: '50/5m' }, + rate_rcpt_null: { enabled: false, default: 1 }, + redis: { database: 4, socket: { host: '127.0.0.1', port: '6379' } }, + concurrency: { plugin: 'karma', good: 10, bad: 1, none: 2 }, + concurrency_history: { enabled: false }, +} describe('plugin_setup', function () { + before(function () { + this.plugin = new fixtures.plugin('index') + this.plugin.config = this.plugin.config.module_config(path.resolve('test')) + }) - before(function () { - this.plugin = new fixtures.plugin('index'); - this.plugin.config = this.plugin.config.module_config(path.resolve('test')); - }) + it('loads config', function () { + // gotta inherit b/c config loader merges in defaults from redis.ini + this.plugin.inherits('haraka-plugin-redis') + this.plugin.load_limit_ini() + assert.deepEqual(this.plugin.cfg, default_config) // loaded config + }) - it('loads config', function () { - // gotta inherit b/c config loader merges in defaults from redis.ini - this.plugin.inherits('haraka-plugin-redis'); - this.plugin.load_limit_ini(); - assert.deepEqual(this.plugin.cfg, default_config); // loaded config - }) - - it('registers', function () { - this.plugin.register(); - assert.deepEqual(this.plugin.cfg, default_config); - }) + it('registers', function () { + this.plugin.register() + assert.deepEqual(this.plugin.cfg, default_config) + }) }) diff --git a/test/history.js b/test/history.js index ca2c0d2..e2e6632 100644 --- a/test/history.js +++ b/test/history.js @@ -1,52 +1,50 @@ -'use strict'; const assert = require('assert') -const path = require('path') +const path = require('path') // const constants = require('haraka-constants'); -const fixtures = require('haraka-test-fixtures'); +const fixtures = require('haraka-test-fixtures') describe('get_history_limit', function () { - - before(function () { - this.plugin = new fixtures.plugin('index'); - this.plugin.config = this.plugin.config.module_config(path.resolve('test')); - - this.connection = new fixtures.connection.createConnection(); - this.connection.transaction = new fixtures.transaction.createTransaction(); - - this.plugin.register(); - - this.plugin.cfg.concurrency_history = { - enabled: true, - plugin: 'karma', - good: 5, - bad: 1, - none: 2, - }; - }) - - it('good', function () { - this.connection.results.add({name: 'karma'}, { history: 1 }); - assert.equal( - 5, - this.plugin.get_history_limit('concurrency', this.connection) - ); - }) - - it('bad', function () { - this.connection.results.add({name: 'karma'}, { history: -1 }); - assert.equal( - 1, - this.plugin.get_history_limit('concurrency', this.connection) - ); - }) - - it('none', function () { - this.connection.results.add({name: 'karma'}, { history: 0 }); - assert.equal( - 2, - this.plugin.get_history_limit('concurrency', this.connection) - ); - }) + before(function () { + this.plugin = new fixtures.plugin('index') + this.plugin.config = this.plugin.config.module_config(path.resolve('test')) + + this.connection = new fixtures.connection.createConnection() + this.connection.init_transaction() + + this.plugin.register() + + this.plugin.cfg.concurrency_history = { + enabled: true, + plugin: 'karma', + good: 5, + bad: 1, + none: 2, + } + }) + + it('good', function () { + this.connection.results.add({ name: 'karma' }, { history: 1 }) + assert.equal( + 5, + this.plugin.get_history_limit('concurrency', this.connection), + ) + }) + + it('bad', function () { + this.connection.results.add({ name: 'karma' }, { history: -1 }) + assert.equal( + 1, + this.plugin.get_history_limit('concurrency', this.connection), + ) + }) + + it('none', function () { + this.connection.results.add({ name: 'karma' }, { history: 0 }) + assert.equal( + 2, + this.plugin.get_history_limit('concurrency', this.connection), + ) + }) }) diff --git a/test/inheritance.js b/test/inheritance.js index a43d442..2eccec4 100644 --- a/test/inheritance.js +++ b/test/inheritance.js @@ -1,28 +1,26 @@ -'use strict'; const assert = require('assert') -const fixtures = require('haraka-test-fixtures'); +const fixtures = require('haraka-test-fixtures') describe('inheritance', function () { + beforeEach(function () { + this.plugin = new fixtures.plugin('index') + }) - beforeEach(function () { - this.plugin = new fixtures.plugin('index'); - }) + it('inherits redis', function () { + this.plugin.inherits('haraka-plugin-redis') + assert.equal(typeof this.plugin.load_redis_ini, 'function') + }) - it('inherits redis', function () { - this.plugin.inherits('haraka-plugin-redis'); - assert.equal(typeof this.plugin.load_redis_ini, 'function'); - }) + it('can call parent functions', function () { + this.plugin.inherits('haraka-plugin-redis') + this.plugin.load_redis_ini() + assert.ok(this.plugin.redisCfg) // loaded config + }) - it('can call parent functions', function () { - this.plugin.inherits('haraka-plugin-redis'); - this.plugin.load_redis_ini(); - assert.ok(this.plugin.redisCfg); // loaded config - }) - - it('register', function () { - this.plugin.register(); - assert.ok(this.plugin.cfg); // loaded config - }) -}) \ No newline at end of file + it('register', function () { + this.plugin.register() + assert.ok(this.plugin.cfg) // loaded config + }) +}) diff --git a/test/limit.js b/test/limit.js index 7913682..d7d4ea3 100644 --- a/test/limit.js +++ b/test/limit.js @@ -1,135 +1,131 @@ -'use strict'; -const assert = require('assert') -const path = require('path'); +const assert = require('assert') +const path = require('path') -const constants = require('haraka-constants'); -const fixtures = require('haraka-test-fixtures'); +const constants = require('haraka-constants') +const fixtures = require('haraka-test-fixtures') -function setUp () { - this.plugin = new fixtures.plugin('index'); - this.plugin.config = this.plugin.config.module_config(path.resolve('test')); +function setUp() { + this.plugin = new fixtures.plugin('index') + this.plugin.config = this.plugin.config.module_config(path.resolve('test')) - this.connection = new fixtures.connection.createConnection(); - this.connection.transaction = new fixtures.transaction.createTransaction(); + this.connection = new fixtures.connection.createConnection() + this.connection.init_transaction() - this.plugin.register(); + this.plugin.register() } describe('max_errors', function () { - - before(setUp) - - it('none', function (done) { - // console.log(this); - this.plugin.max_errors(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, null); - assert.equal(msg, null); - done(); - }, this.connection); - }) - - it('too many', function (done) { - // console.log(this); - this.connection.errors=10; - this.plugin.cfg.errors = { max: 9 }; - this.plugin.max_errors(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, constants.DENYSOFTDISCONNECT); - assert.equal(msg, 'Too many errors'); - done(); - }, this.connection); - }) + before(setUp) + + it('none', function (done) { + // console.log(this); + this.plugin.max_errors(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, null) + assert.equal(msg, null) + done() + }, this.connection) + }) + + it('too many', function (done) { + // console.log(this); + this.connection.errors = 10 + this.plugin.cfg.errors = { max: 9 } + this.plugin.max_errors(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, constants.DENYSOFTDISCONNECT) + assert.equal(msg, 'Too many errors') + done() + }, this.connection) + }) }) describe('max_recipients', function () { - - before(setUp) - - it('none', function (done) { - this.plugin.max_recipients(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, null); - assert.equal(msg, null); - done(); - }, this.connection); - }) - - it('too many', function (done) { - this.connection.rcpt_count = { accept: 3, tempfail: 5, reject: 4 }; - this.plugin.cfg.recipients = { max: 10 }; - this.plugin.max_recipients(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, constants.DENYSOFT); - assert.equal(msg, 'Too many recipient attempts'); - done(); - }, this.connection); - }) + before(setUp) + + it('none', function (done) { + this.plugin.max_recipients(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, null) + assert.equal(msg, null) + done() + }, this.connection) + }) + + it('too many', function (done) { + this.connection.rcpt_count = { accept: 3, tempfail: 5, reject: 4 } + this.plugin.cfg.recipients = { max: 10 } + this.plugin.max_recipients(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, constants.DENYSOFT) + assert.equal(msg, 'Too many recipient attempts') + done() + }, this.connection) + }) }) describe('max_unrecognized_commands', function () { - before(setUp); - - it('none', function (done) { - // console.log(this); - this.plugin.max_unrecognized_commands(function (rc, msg) { - assert.equal(rc, null); - assert.equal(msg, null); - done(); - }, this.connection); - }) - - it('too many', function (done) { - // console.log(this); - this.plugin.cfg.unrecognized_commands = { max: 5 }; - this.connection.results.push(this.plugin, { - 'unrec_cmds': ['1','2','3','4',5,6] - }); - this.plugin.max_unrecognized_commands(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, constants.DENYSOFTDISCONNECT); - assert.equal(msg, 'Too many unrecognized commands'); - done(); - }, this.connection); + before(setUp) + + it('none', function (done) { + // console.log(this); + this.plugin.max_unrecognized_commands(function (rc, msg) { + assert.equal(rc, null) + assert.equal(msg, null) + done() + }, this.connection) + }) + + it('too many', function (done) { + // console.log(this); + this.plugin.cfg.unrecognized_commands = { max: 5 } + this.connection.results.push(this.plugin, { + unrec_cmds: ['1', '2', '3', '4', 5, 6], }) + this.plugin.max_unrecognized_commands(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, constants.DENYSOFTDISCONNECT) + assert.equal(msg, 'Too many unrecognized commands') + done() + }, this.connection) + }) }) - describe('check_concurrency', function () { - before(setUp); - - it('none', function (done) { - this.plugin.check_concurrency(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, null); - assert.equal(msg, null); - done(); - }, this.connection); - }) - - it('at max', function (done) { - this.plugin.cfg.concurrency.history = undefined; - this.plugin.cfg.concurrency = { max: 4 }; - this.connection.results.add(this.plugin, { concurrent_count: 4 }); - this.plugin.check_concurrency(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, null); - assert.equal(msg, null); - done(); - }, this.connection); - }) - - it('too many', function (done) { - this.plugin.cfg.concurrency.history = undefined; - this.plugin.cfg.concurrency = { max: 4 }; - this.plugin.cfg.concurrency.disconnect_delay=1; - this.connection.results.add(this.plugin, { concurrent_count: 5 }); - this.plugin.check_concurrency(function (rc, msg) { - // console.log(arguments); - assert.equal(rc, constants.DENYSOFTDISCONNECT); - assert.equal(msg, 'Too many concurrent connections'); - done(); - }, this.connection); - }) + before(setUp) + + it('none', function (done) { + this.plugin.check_concurrency(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, null) + assert.equal(msg, null) + done() + }, this.connection) + }) + + it('at max', function (done) { + this.plugin.cfg.concurrency.history = undefined + this.plugin.cfg.concurrency = { max: 4 } + this.connection.results.add(this.plugin, { concurrent_count: 4 }) + this.plugin.check_concurrency(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, null) + assert.equal(msg, null) + done() + }, this.connection) + }) + + it('too many', function (done) { + this.plugin.cfg.concurrency.history = undefined + this.plugin.cfg.concurrency = { max: 4 } + this.plugin.cfg.concurrency.disconnect_delay = 1 + this.connection.results.add(this.plugin, { concurrent_count: 5 }) + this.plugin.check_concurrency(function (rc, msg) { + // console.log(arguments); + assert.equal(rc, constants.DENYSOFTDISCONNECT) + assert.equal(msg, 'Too many concurrent connections') + done() + }, this.connection) + }) }) diff --git a/test/outbound.js b/test/outbound.js index 6442a7e..4ef230a 100644 --- a/test/outbound.js +++ b/test/outbound.js @@ -1,43 +1,45 @@ -'use strict'; -const assert = require('assert') +const assert = require('assert') // var Address = require('address-rfc2821').Address; -const constants = require('haraka-constants'); -const fixtures = require('haraka-test-fixtures'); +const constants = require('haraka-constants') +const fixtures = require('haraka-test-fixtures') -function _set_up (done) { - this.plugin = new fixtures.plugin('index'); - this.plugin.register(); - this.server = { notes: {} }; - this.plugin.init_redis_plugin(function () { - done(); - }, - this.server); +function _set_up(done) { + this.plugin = new fixtures.plugin('index') + this.plugin.register() + this.server = { notes: {} } + this.plugin.init_redis_plugin(function () { + done() + }, this.server) } describe('outbound_increment', function () { - before(_set_up); + before(_set_up) - it('no limit, no delay', function (done) { - this.plugin.outbound_increment(function (code, msg) { - assert.equal(code, undefined); - assert.equal(msg, undefined); - done(); - }, - { domain: 'test.com'}); - }) + it('no limit, no delay', function (done) { + this.plugin.outbound_increment( + function (code, msg) { + assert.equal(code, undefined) + assert.equal(msg, undefined) + done() + }, + { domain: 'test.com' }, + ) + }) - it('limits has delay', function (done) { - const self = this; - self.plugin.cfg.outbound['slow.test.com'] = 3; - self.plugin.db.hSet('outbound-rate:slow.test.com', 'TOTAL', 4).then(() => { - self.plugin.outbound_increment(function (code, delay) { - assert.equal(code, constants.delay); - assert.equal(delay, 30); - done(); - }, - { domain: 'slow.test.com'}); - }) + it('limits has delay', function (done) { + const self = this + self.plugin.cfg.outbound['slow.test.com'] = 3 + self.plugin.db.hSet('outbound-rate:slow.test.com', 'TOTAL', 4).then(() => { + self.plugin.outbound_increment( + function (code, delay) { + assert.equal(code, constants.delay) + assert.equal(delay, 30) + done() + }, + { domain: 'slow.test.com' }, + ) }) -}) \ No newline at end of file + }) +}) diff --git a/test/rate_limit.js b/test/rate_limit.js index 9982a8a..8f38a78 100644 --- a/test/rate_limit.js +++ b/test/rate_limit.js @@ -1,150 +1,165 @@ -'use strict'; -const assert = require('assert') -const path = require('path'); +const assert = require('assert') +const path = require('path') -const Address = require('address-rfc2821').Address; -const constants = require('haraka-constants'); -const fixtures = require('haraka-test-fixtures'); +const Address = require('address-rfc2821').Address +const constants = require('haraka-constants') +const fixtures = require('haraka-test-fixtures') -function setUp () { - this.plugin = new fixtures.plugin('rate_limit'); +function setUp() { + this.plugin = new fixtures.plugin('rate_limit') - this.connection = new fixtures.connection.createConnection(); - this.connection.remote = { ip: '1.2.3.4', host: 'test.com' }; + this.connection = new fixtures.connection.createConnection() + this.connection.remote = { ip: '1.2.3.4', host: 'test.com' } - this.plugin.register(); + this.plugin.register() } describe('get_host_key', function () { - before(setUp) - 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 () { - 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'); - }) + before(setUp) + 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 () { + 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') + }) }) describe('get_mail_key', function () { - beforeEach(function () { - this.plugin = new fixtures.plugin('rate_limit'); - this.connection = new fixtures.connection.createConnection(); - this.plugin.register(); - }) - - it('rate_rcpt_sender', function () { - const [ addr, limit ] = this.plugin.get_mail_key('rate_rcpt_sender', new Address('')) - // console.log(arguments); - assert.equal(addr, 'user@example.com'); - assert.equal(limit, '50/5m'); - }) - it('rate_rcpt_null', function () { - const [ addr, limit ] = this.plugin.get_mail_key('rate_rcpt_null', new Address('')) - // console.log(arguments); - assert.equal(addr, 'postmaster'); - assert.equal(limit, '1'); - }) - it('rate_rcpt', function () { - const [ addr, limit ] = this.plugin.get_mail_key('rate_rcpt', new Address('')) - // console.log(arguments); - assert.equal(addr, 'user@example.com'); - assert.equal(limit, '50/5m'); - }) + beforeEach(function () { + this.plugin = new fixtures.plugin('rate_limit') + this.connection = new fixtures.connection.createConnection() + this.plugin.register() + }) + + it('rate_rcpt_sender', function () { + const [addr, limit] = this.plugin.get_mail_key( + 'rate_rcpt_sender', + new Address(''), + ) + // console.log(arguments); + assert.equal(addr, 'user@example.com') + assert.equal(limit, '50/5m') + }) + it('rate_rcpt_null', function () { + const [addr, limit] = this.plugin.get_mail_key( + 'rate_rcpt_null', + new Address(''), + ) + // console.log(arguments); + assert.equal(addr, 'postmaster') + assert.equal(limit, '1') + }) + it('rate_rcpt', function () { + const [addr, limit] = this.plugin.get_mail_key( + 'rate_rcpt', + new Address(''), + ) + // console.log(arguments); + assert.equal(addr, 'user@example.com') + assert.equal(limit, '50/5m') + }) }) describe('rate_limit', function () { - beforeEach(function (done) { - this.plugin = new fixtures.plugin('rate_limit'); - this.connection = new fixtures.connection.createConnection(); - this.plugin.register(); - const server = { notes: {} }; - this.plugin.init_redis_plugin(function () { - done(); - }, - server); - }) - - 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', async function () { - const is_limited = await this.plugin.rate_limit(this.connection, 'key', '50/5m') - assert.equal(is_limited, false); - }) + beforeEach(function (done) { + this.plugin = new fixtures.plugin('rate_limit') + this.connection = new fixtures.connection.createConnection() + this.plugin.register() + const server = { notes: {} } + this.plugin.init_redis_plugin(function () { + done() + }, server) + }) + + 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', async function () { + const is_limited = await this.plugin.rate_limit( + this.connection, + 'key', + '50/5m', + ) + assert.equal(is_limited, false) + }) }) describe('rate_conn', function () { - beforeEach(function (done) { - this.server = { notes: {} }; - - this.plugin = new fixtures.plugin('rate_limit'); - this.plugin.config = this.plugin.config.module_config(path.resolve('test')); - - this.connection = new fixtures.connection.createConnection(); - this.connection.remote.ip = '1.2.3.4'; - this.connection.remote.host = 'mail.example.com'; - - this.plugin.register(); - this.plugin.init_redis_plugin(function () { - done(); - }, - this.server); - }) - - it('default limit', function (done) { - const plugin = this.plugin; - const connection = this.connection; - - plugin.rate_conn_incr(function () { - plugin.rate_conn_enforce(function (code, msg) { - const rc = connection.results.get(plugin.name); - assert.ok(rc.rate_conn); - - const match = /([\d]+):(.*)$/.exec(rc.rate_conn); // 1/5 - - if (parseInt(match[1]) <= parseInt(match[2])) { - assert.equal(code, undefined); - assert.equal(msg, undefined); - } - else { - assert.equal(code, constants.DENYSOFTDISCONNECT); - assert.equal(msg, 'connection rate limit exceeded'); - } - done(); - }.bind(this), - connection); - }, connection); - }) - - it('defined limit', function (done) { - const plugin = this.plugin; - const 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) { - const rc = connection.results.get(plugin.name); - assert.ok(rc.rate_conn); - const match = /^([\d]+):(.*)$/.exec(rc.rate_conn); // 1/5m - if (parseInt(match[1]) <= parseInt(match[2])) { - assert.equal(code, undefined); - assert.equal(msg, undefined); - } - else { - assert.equal(code, constants.DENYSOFTDISCONNECT); - assert.equal(msg, 'connection rate limit exceeded'); - } - done(); - }.bind(this), - connection); - }, connection); - }) -}) \ No newline at end of file + beforeEach(function (done) { + this.server = { notes: {} } + + this.plugin = new fixtures.plugin('rate_limit') + this.plugin.config = this.plugin.config.module_config(path.resolve('test')) + + this.connection = new fixtures.connection.createConnection() + this.connection.remote.ip = '1.2.3.4' + this.connection.remote.host = 'mail.example.com' + + this.plugin.register() + this.plugin.init_redis_plugin(function () { + done() + }, this.server) + }) + + it('default limit', function (done) { + const plugin = this.plugin + const connection = this.connection + + plugin.rate_conn_incr(function () { + plugin.rate_conn_enforce( + function (code, msg) { + const rc = connection.results.get(plugin.name) + assert.ok(rc.rate_conn) + + const match = /([\d]+):(.*)$/.exec(rc.rate_conn) // 1/5 + + if (parseInt(match[1]) <= parseInt(match[2])) { + assert.equal(code, undefined) + assert.equal(msg, undefined) + } else { + assert.equal(code, constants.DENYSOFTDISCONNECT) + assert.equal(msg, 'connection rate limit exceeded') + } + done() + }.bind(this), + connection, + ) + }, connection) + }) + + it('defined limit', function (done) { + const plugin = this.plugin + const 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) { + const rc = connection.results.get(plugin.name) + assert.ok(rc.rate_conn) + const match = /^([\d]+):(.*)$/.exec(rc.rate_conn) // 1/5m + if (parseInt(match[1]) <= parseInt(match[2])) { + assert.equal(code, undefined) + assert.equal(msg, undefined) + } else { + assert.equal(code, constants.DENYSOFTDISCONNECT) + assert.equal(msg, 'connection rate limit exceeded') + } + done() + }.bind(this), + connection, + ) + }, connection) + }) +})