| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- import assert from 'node:assert/strict';
- import { mkdtempSync } from 'node:fs';
- import net from 'node:net';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import { test } from 'node:test';
- import { AuthenticationRateLimiter } from '../src/auth-rate-limit.js';
- import { createDomain, createInboundMailbox, createUser, initDatabase } from '../src/db.js';
- import { startMailboxAccessServers } from '../src/mail-access.js';
- import { startSubmissionServer } from '../src/submission.js';
- test('IMAP, POP3 and SMTP share generic authentication throttling by IP and account', async () => {
- initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-auth-rate-limit-')), 'auth-rate-limit-secret');
- const user = createUser({
- username: 'auth-rate-limit-user',
- email: 'auth-rate-limit-user@example.com',
- password: 'account-password'
- });
- createDomain(user.id, {
- domain: 'auth-rate-limit.example',
- selector: 'mh',
- verificationToken: 'verify',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: 'mail.auth-rate-limit.example',
- sendingIp: '192.0.2.45',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- const mailbox = createInboundMailbox(user.id, {
- address: 'admin@auth-rate-limit.example',
- password: 'correct-password'
- });
- const authRateLimiter = new AuthenticationRateLimiter({
- combinationLimit: 3,
- accountLimit: 10,
- ipLimit: 20
- });
- const [imapServer, pop3Server] = startMailboxAccessServers({
- hostname: 'mail.auth-rate-limit.example',
- imapEnabled: true,
- imapListeners: [{ port: 0, protocol: 'imap' }],
- pop3Enabled: true,
- pop3Listeners: [{ port: 0, protocol: 'pop3' }],
- allowInsecureAuth: true,
- authRateLimiter
- });
- const [smtpServer] = startSubmissionServer({
- enabled: true,
- listeners: [{ port: 0, protocol: 'smtp' }],
- hostname: 'mail.auth-rate-limit.example',
- allowInsecureAuth: true,
- inboundEnabled: true,
- authRateLimiter
- });
- const servers = [imapServer, pop3Server, smtpServer];
- await Promise.all(servers.map(waitForListening));
- const clients = [];
- try {
- const imap = await connectClient(imapServer.address().port);
- clients.push(imap);
- await imap.readUntil(/\* OK .* IMAP ready\r\n/);
- assert.match(
- await imap.command(`A1 LOGIN "${mailbox.address}" "wrong-imap"`, /A1 NO/),
- /^A1 NO Authentication failed\r\n$/
- );
- const pop3 = await connectClient(pop3Server.address().port);
- clients.push(pop3);
- await pop3.readUntil(/\+OK .* POP3 ready\r\n/);
- assert.match(await pop3.command(`USER ${mailbox.address}`, /\+OK|\-ERR/), /^\+OK User accepted\r\n$/);
- assert.match(await pop3.command('PASS wrong-pop3', /\+OK|\-ERR/), /^\-ERR Authentication failed\r\n$/);
- const smtp = await connectClient(smtpServer.address().port);
- clients.push(smtp);
- await smtp.readUntil(/^220 .* ready\r\n/m);
- await smtp.command('EHLO client.example', /250 HELP\r\n/);
- const wrongAuth = Buffer.from(`\u0000${mailbox.address}\u0000wrong-smtp`).toString('base64');
- assert.match(
- await smtp.command(`AUTH PLAIN ${wrongAuth}`, /535 /),
- /^535 Authentication failed\r\n$/
- );
- const blocked = await connectClient(smtpServer.address().port);
- clients.push(blocked);
- await blocked.readUntil(/^220 .* ready\r\n/m);
- await blocked.command('EHLO client.example', /250 HELP\r\n/);
- const correctAuth = Buffer.from(`\u0000${mailbox.address}\u0000correct-password`).toString('base64');
- assert.match(
- await blocked.command(`AUTH PLAIN ${correctAuth}`, /535 /),
- /^535 Authentication failed\r\n$/
- );
- } finally {
- for (const client of clients) client.close();
- await Promise.all(servers.map(closeServer));
- }
- });
- function connectClient(port) {
- return new Promise((resolve, reject) => {
- const socket = net.createConnection({ host: '127.0.0.1', port });
- socket.setTimeout(5_000);
- let buffer = '';
- const waiters = [];
- socket.on('data', (chunk) => {
- buffer += chunk.toString('utf8');
- for (const waiter of [...waiters]) {
- if (!waiter.pattern.test(buffer)) continue;
- waiters.splice(waiters.indexOf(waiter), 1);
- const output = buffer;
- buffer = '';
- clearTimeout(waiter.timer);
- waiter.resolve(output);
- }
- });
- socket.once('connect', () => resolve({
- command(command, pattern) {
- socket.write(`${command}\r\n`);
- return this.readUntil(pattern);
- },
- readUntil(pattern) {
- if (pattern.test(buffer)) {
- const output = buffer;
- buffer = '';
- return Promise.resolve(output);
- }
- return new Promise((waitResolve, waitReject) => {
- const waiter = { pattern, resolve: waitResolve, timer: null };
- waiter.timer = setTimeout(() => {
- waiters.splice(waiters.indexOf(waiter), 1);
- waitReject(new Error(`Timed out waiting for ${pattern}; buffered response: ${buffer}`));
- }, 5_000);
- waiters.push(waiter);
- });
- },
- close() {
- socket.destroy();
- }
- }));
- socket.once('error', reject);
- socket.once('timeout', () => reject(new Error('Mail protocol client timed out')));
- });
- }
- function waitForListening(server) {
- if (server.listening) return Promise.resolve();
- return new Promise((resolve) => server.once('listening', resolve));
- }
- function closeServer(server) {
- return new Promise((resolve, reject) => {
- server.close((error) => error ? reject(error) : resolve());
- });
- }
|