Haraka 3.1.4 → 3.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +1 -1
- package/{Changes.md → CHANGELOG.md} +34 -0
- package/CONTRIBUTORS.md +26 -26
- package/README.md +68 -93
- package/SECURITY.md +178 -0
- package/bin/haraka +7 -14
- package/config/plugins +0 -3
- package/docs/Connection.md +126 -39
- package/docs/CoreConfig.md +92 -74
- package/docs/HAProxy.md +41 -25
- package/docs/Logging.md +68 -38
- package/docs/Outbound.md +124 -179
- package/docs/Plugins.md +38 -59
- package/docs/Transaction.md +78 -83
- package/docs/Tutorial.md +122 -209
- package/docs/plugins/aliases.md +1 -141
- package/docs/plugins/auth/auth_ldap.md +2 -39
- package/docs/plugins/max_unrecognized_commands.md +4 -18
- package/docs/plugins/process_title.md +3 -3
- package/docs/plugins/reseed_rng.md +11 -13
- package/docs/plugins/tls.md +7 -7
- package/docs/plugins/toobusy.md +10 -4
- package/docs/tutorials/SettingUpOutbound.md +40 -48
- package/endpoint.js +32 -2
- package/outbound/hmail.js +3 -2
- package/outbound/index.js +3 -0
- package/package.json +21 -34
- package/plugins/queue/smtp_forward.js +4 -4
- package/run_tests +3 -15
- package/server.js +17 -7
- package/smtp_client.js +8 -6
- package/test/connection.js +234 -0
- package/test/endpoint.js +32 -4
- package/test/host_pool.js +57 -31
- package/test/logger.js +75 -135
- package/test/outbound/bounce_net_errors.js +87 -131
- package/test/outbound/bounce_rfc3464.js +177 -254
- package/test/outbound/hmail.js +19 -0
- package/test/outbound/index.js +189 -0
- package/test/outbound/queue.js +92 -0
- package/test/plugins/auth/auth_base.js +39 -44
- package/test/plugins/auth/auth_vpopmaild.js +8 -9
- package/test/plugins/queue/smtp_forward.js +953 -183
- package/test/plugins/rcpt_to.host_list_base.js +58 -93
- package/test/plugins/rcpt_to.in_host_list.js +126 -175
- package/test/plugins/record_envelope_addresses.js +8 -8
- package/test/plugins/status.js +10 -10
- package/test/plugins/tls.js +9 -19
- package/test/plugins/xclient.js +75 -110
- package/test/plugins.js +10 -13
- package/test/rfc1869.js +50 -70
- package/test/server.js +438 -421
- package/test/smtp_client.js +1192 -218
- package/test/tls_socket.js +242 -0
- package/tls_socket.js +18 -22
package/server.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
// smtp network server
|
|
3
3
|
|
|
4
4
|
const cluster = require('node:cluster')
|
|
5
|
+
const { spawn } = require('node:child_process')
|
|
5
6
|
const fs = require('node:fs')
|
|
6
7
|
const os = require('node:os')
|
|
7
8
|
const path = require('node:path')
|
|
8
9
|
const tls = require('node:tls')
|
|
9
|
-
|
|
10
|
-
const daemon = require('daemon')
|
|
11
10
|
const constants = require('haraka-constants')
|
|
12
11
|
|
|
13
12
|
const tls_socket = require('./tls_socket')
|
|
@@ -77,15 +76,26 @@ Server.daemonize = function () {
|
|
|
77
76
|
// we get a spurious 'Exiting' log entry.
|
|
78
77
|
process.removeAllListeners('exit')
|
|
79
78
|
Server.lognotice('Daemonizing...')
|
|
80
|
-
}
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const log_fd = fs.openSync(c.daemon_log_file, 'a')
|
|
81
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
82
|
+
detached: true,
|
|
83
|
+
stdio: ['ignore', log_fd, log_fd],
|
|
84
|
+
env: { ...process.env, __daemon: '1' },
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
})
|
|
87
|
+
child.unref()
|
|
88
|
+
process.exit(0)
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
// We are the daemon from here on...
|
|
86
|
-
const npid = require('npid')
|
|
87
92
|
try {
|
|
88
|
-
|
|
93
|
+
fs.writeFileSync(c.daemon_pid_file, `${process.pid}\n`, { flag: 'wx' })
|
|
94
|
+
process.on('exit', () => {
|
|
95
|
+
try {
|
|
96
|
+
fs.unlinkSync(c.daemon_pid_file)
|
|
97
|
+
} catch {}
|
|
98
|
+
})
|
|
89
99
|
} catch (err) {
|
|
90
100
|
Server.logerror(err.message)
|
|
91
101
|
logger.dump_and_exit(1)
|
package/smtp_client.js
CHANGED
|
@@ -216,8 +216,7 @@ class SMTPClient extends events.EventEmitter {
|
|
|
216
216
|
|
|
217
217
|
release() {
|
|
218
218
|
if (this.state === STATE.DESTROYED) return
|
|
219
|
-
|
|
220
|
-
;[
|
|
219
|
+
const listeners = [
|
|
221
220
|
'auth',
|
|
222
221
|
'bad_code',
|
|
223
222
|
'capabilities',
|
|
@@ -233,9 +232,11 @@ class SMTPClient extends events.EventEmitter {
|
|
|
233
232
|
'rset',
|
|
234
233
|
'server_protocol',
|
|
235
234
|
'xclient',
|
|
236
|
-
]
|
|
235
|
+
]
|
|
236
|
+
logger.debug(`[smtp_client] ${this.uuid} releasing, state=${this.state}`)
|
|
237
|
+
for (const l of listeners) {
|
|
237
238
|
this.removeAllListeners(l)
|
|
238
|
-
}
|
|
239
|
+
}
|
|
239
240
|
|
|
240
241
|
if (this.connected) this.send_command('QUIT')
|
|
241
242
|
this.destroy()
|
|
@@ -349,7 +350,7 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
349
350
|
}
|
|
350
351
|
}
|
|
351
352
|
|
|
352
|
-
const hostport = get_hostport(connection,
|
|
353
|
+
const hostport = get_hostport(connection, c)
|
|
353
354
|
const smtp_client = new SMTPClient(hostport)
|
|
354
355
|
logger.info(`[smtp_client] uuid=${smtp_client.uuid} host=${hostport.host} port=${hostport.port} created`)
|
|
355
356
|
|
|
@@ -468,7 +469,8 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
468
469
|
callback(null, smtp_client)
|
|
469
470
|
}
|
|
470
471
|
|
|
471
|
-
function get_hostport(connection,
|
|
472
|
+
function get_hostport(connection, cfg) {
|
|
473
|
+
const server = connection.server
|
|
472
474
|
if (cfg.forwarding_host_pool) {
|
|
473
475
|
if (!server.notes.host_pool) {
|
|
474
476
|
connection.logwarn(`creating host_pool from ${cfg.forwarding_host_pool}`)
|
package/test/connection.js
CHANGED
|
@@ -5,6 +5,7 @@ const assert = require('node:assert/strict')
|
|
|
5
5
|
|
|
6
6
|
const constants = require('haraka-constants')
|
|
7
7
|
const DSN = require('haraka-dsn')
|
|
8
|
+
const { Address } = require('address-rfc2821')
|
|
8
9
|
|
|
9
10
|
const connection = require('../connection')
|
|
10
11
|
const Server = require('../server')
|
|
@@ -442,4 +443,237 @@ describe('connection', () => {
|
|
|
442
443
|
})
|
|
443
444
|
})
|
|
444
445
|
})
|
|
446
|
+
|
|
447
|
+
describe('queue responses', () => {
|
|
448
|
+
beforeEach(setUp)
|
|
449
|
+
|
|
450
|
+
const prepQueueTestConnection = () => {
|
|
451
|
+
const calls = { respond: [], reset: 0, disconnect: 0, queue_ok: 0, results: [] }
|
|
452
|
+
const plugins = require('../plugins')
|
|
453
|
+
const originalRunHooks = plugins.run_hooks
|
|
454
|
+
|
|
455
|
+
this.connection.transaction = {
|
|
456
|
+
uuid: 'txn-123',
|
|
457
|
+
msg_status: null,
|
|
458
|
+
results: {
|
|
459
|
+
add(_meta, payload) {
|
|
460
|
+
calls.results.push(payload)
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.connection.respond = (code, msg, cb) => {
|
|
466
|
+
calls.respond.push({ code, msg })
|
|
467
|
+
if (cb) cb()
|
|
468
|
+
}
|
|
469
|
+
this.connection.reset_transaction = (cb) => {
|
|
470
|
+
calls.reset++
|
|
471
|
+
this.connection.transaction = this.connection.transaction || {}
|
|
472
|
+
if (cb) cb()
|
|
473
|
+
}
|
|
474
|
+
this.connection.disconnect = () => {
|
|
475
|
+
calls.disconnect++
|
|
476
|
+
}
|
|
477
|
+
plugins.run_hooks = (hook) => {
|
|
478
|
+
if (hook === 'queue_ok') calls.queue_ok++
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
calls,
|
|
483
|
+
restore() {
|
|
484
|
+
plugins.run_hooks = originalRunHooks
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
it('queue_respond handles denydisconnect and marks message rejected', () => {
|
|
490
|
+
const harness = prepQueueTestConnection()
|
|
491
|
+
try {
|
|
492
|
+
this.connection.queue_respond(constants.denydisconnect)
|
|
493
|
+
assert.equal(harness.calls.respond[0].code, 550)
|
|
494
|
+
assert.equal(this.connection.msg_count.reject, 1)
|
|
495
|
+
assert.equal(this.connection.transaction.msg_status, 'rejected')
|
|
496
|
+
assert.equal(harness.calls.disconnect, 1)
|
|
497
|
+
assert.deepEqual(harness.calls.results[0], { fail: 'Message denied' })
|
|
498
|
+
} finally {
|
|
499
|
+
harness.restore()
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('queue_respond handles denysoft and resets transaction', () => {
|
|
504
|
+
const harness = prepQueueTestConnection()
|
|
505
|
+
try {
|
|
506
|
+
this.connection.queue_respond(constants.denysoft)
|
|
507
|
+
assert.equal(harness.calls.respond[0].code, 450)
|
|
508
|
+
assert.equal(this.connection.msg_count.tempfail, 1)
|
|
509
|
+
assert.equal(this.connection.transaction.msg_status, 'deferred')
|
|
510
|
+
assert.equal(harness.calls.reset, 1)
|
|
511
|
+
assert.deepEqual(harness.calls.results[0], { fail: 'Message denied temporarily' })
|
|
512
|
+
} finally {
|
|
513
|
+
harness.restore()
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('queue_respond handles denysoftdisconnect and disconnects', () => {
|
|
518
|
+
const harness = prepQueueTestConnection()
|
|
519
|
+
try {
|
|
520
|
+
this.connection.queue_respond(constants.denysoftdisconnect)
|
|
521
|
+
assert.equal(harness.calls.respond[0].code, 450)
|
|
522
|
+
assert.equal(this.connection.msg_count.tempfail, 1)
|
|
523
|
+
assert.equal(this.connection.transaction.msg_status, 'deferred')
|
|
524
|
+
assert.equal(harness.calls.disconnect, 1)
|
|
525
|
+
} finally {
|
|
526
|
+
harness.restore()
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('queue_respond default path returns 451 and resets transaction', () => {
|
|
531
|
+
const harness = prepQueueTestConnection()
|
|
532
|
+
try {
|
|
533
|
+
this.connection.queue_respond(constants.cont)
|
|
534
|
+
assert.equal(harness.calls.respond[0].code, 451)
|
|
535
|
+
assert.equal(this.connection.msg_count.tempfail, 1)
|
|
536
|
+
assert.equal(this.connection.transaction.msg_status, 'deferred')
|
|
537
|
+
assert.equal(harness.calls.reset, 1)
|
|
538
|
+
} finally {
|
|
539
|
+
harness.restore()
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('queue_ok_respond accepts and resets transaction', () => {
|
|
544
|
+
const harness = prepQueueTestConnection()
|
|
545
|
+
try {
|
|
546
|
+
this.connection.queue_ok_respond(constants.ok, null, 'queued')
|
|
547
|
+
assert.equal(harness.calls.respond[0].code, 250)
|
|
548
|
+
assert.equal(this.connection.msg_count.accept, 1)
|
|
549
|
+
assert.equal(this.connection.transaction.msg_status, 'accepted')
|
|
550
|
+
assert.equal(harness.calls.reset, 1)
|
|
551
|
+
} finally {
|
|
552
|
+
harness.restore()
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
describe('smtp command/response branches', () => {
|
|
558
|
+
beforeEach(setUp)
|
|
559
|
+
|
|
560
|
+
it('rcpt_respond deny removes recipient and records reject', () => {
|
|
561
|
+
const plugins = require('../plugins')
|
|
562
|
+
const originalRunHooks = plugins.run_hooks
|
|
563
|
+
const rcpt = new Address('<to@example.com>')
|
|
564
|
+
const sender = new Address('<from@example.com>')
|
|
565
|
+
const actions = []
|
|
566
|
+
|
|
567
|
+
this.connection.transaction = {
|
|
568
|
+
rcpt_to: [rcpt],
|
|
569
|
+
mail_from: sender,
|
|
570
|
+
results: { push() {} },
|
|
571
|
+
}
|
|
572
|
+
this.connection.rcpt_incr = (_rcpt, action) => actions.push(action)
|
|
573
|
+
this.connection.respond = (_code, _msg, cb) => cb && cb()
|
|
574
|
+
plugins.run_hooks = () => {}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
this.connection.rcpt_respond(constants.deny, 'no')
|
|
578
|
+
assert.equal(actions[0], 'reject')
|
|
579
|
+
assert.equal(this.connection.transaction.rcpt_to.length, 0)
|
|
580
|
+
} finally {
|
|
581
|
+
plugins.run_hooks = originalRunHooks
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('rcpt_respond ok runs rcpt_ok hook', () => {
|
|
586
|
+
const plugins = require('../plugins')
|
|
587
|
+
const originalRunHooks = plugins.run_hooks
|
|
588
|
+
const rcpt = new Address('<to@example.com>')
|
|
589
|
+
const sender = new Address('<from@example.com>')
|
|
590
|
+
const hooks = []
|
|
591
|
+
|
|
592
|
+
this.connection.transaction = {
|
|
593
|
+
rcpt_to: [rcpt],
|
|
594
|
+
mail_from: sender,
|
|
595
|
+
results: { push() {} },
|
|
596
|
+
}
|
|
597
|
+
this.connection.respond = (_code, _msg, cb) => cb && cb()
|
|
598
|
+
plugins.run_hooks = (hook) => hooks.push(hook)
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
this.connection.rcpt_respond(constants.ok, 'ok')
|
|
602
|
+
assert.equal(hooks.includes('rcpt_ok'), true)
|
|
603
|
+
assert.equal(this.connection.last_rcpt_msg, 'ok')
|
|
604
|
+
} finally {
|
|
605
|
+
plugins.run_hooks = originalRunHooks
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
it('cmd_proxy rejects when not allowed', () => {
|
|
610
|
+
let code
|
|
611
|
+
this.connection.proxy.allowed = false
|
|
612
|
+
this.connection.respond = (c) => {
|
|
613
|
+
code = c
|
|
614
|
+
}
|
|
615
|
+
this.connection.disconnect = () => {}
|
|
616
|
+
this.connection.cmd_proxy('TCP4 1.2.3.4 5.6.7.8 100 25')
|
|
617
|
+
assert.equal(code, 421)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('cmd_proxy accepts valid TCP4 proxy line and runs connect_init', () => {
|
|
621
|
+
const plugins = require('../plugins')
|
|
622
|
+
const originalRunHooks = plugins.run_hooks
|
|
623
|
+
const hooks = []
|
|
624
|
+
this.connection.proxy.allowed = true
|
|
625
|
+
this.connection.remote.ip = '10.0.0.1'
|
|
626
|
+
this.connection.reset_transaction = (cb) => cb && cb()
|
|
627
|
+
this.connection.respond = () => {}
|
|
628
|
+
plugins.run_hooks = (hook) => hooks.push(hook)
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
this.connection.cmd_proxy('TCP4 1.2.3.4 5.6.7.8 100 25')
|
|
632
|
+
assert.equal(this.connection.proxy.type, 'haproxy')
|
|
633
|
+
assert.equal(this.connection.remote.ip, '1.2.3.4')
|
|
634
|
+
assert.equal(this.connection.local.ip, '5.6.7.8')
|
|
635
|
+
assert.equal(hooks.includes('connect_init'), true)
|
|
636
|
+
} finally {
|
|
637
|
+
plugins.run_hooks = originalRunHooks
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
it('cmd_data validates argument/transaction/recipient preconditions', () => {
|
|
642
|
+
const responses = []
|
|
643
|
+
this.connection.respond = (code, msg) => {
|
|
644
|
+
responses.push([code, msg])
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.connection.cmd_data('unexpected')
|
|
648
|
+
this.connection.cmd_data()
|
|
649
|
+
this.connection.transaction = { rcpt_to: [] }
|
|
650
|
+
this.connection.cmd_data()
|
|
651
|
+
|
|
652
|
+
assert.equal(responses[0][0], 501)
|
|
653
|
+
assert.equal(responses[1][0], 503)
|
|
654
|
+
assert.equal(responses[2][0], 503)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it('data_respond denysoftdisconnect disconnects and default enters DATA', () => {
|
|
658
|
+
const responses = []
|
|
659
|
+
let disconnected = 0
|
|
660
|
+
this.connection.transaction = { data_bytes: 5 }
|
|
661
|
+
this.connection.respond = (code, _msg, cb) => {
|
|
662
|
+
responses.push(code)
|
|
663
|
+
if (cb) cb()
|
|
664
|
+
}
|
|
665
|
+
this.connection.disconnect = () => {
|
|
666
|
+
disconnected++
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
this.connection.data_respond(constants.denysoftdisconnect, 'tmpfail')
|
|
670
|
+
this.connection.data_respond(constants.ok, 'ok')
|
|
671
|
+
|
|
672
|
+
assert.equal(responses[0], 451)
|
|
673
|
+
assert.equal(disconnected, 1)
|
|
674
|
+
assert.equal(responses[1], 354)
|
|
675
|
+
assert.equal(this.connection.state, constants.connection.state.DATA)
|
|
676
|
+
assert.equal(this.connection.transaction.data_bytes, 0)
|
|
677
|
+
})
|
|
678
|
+
})
|
|
445
679
|
})
|
package/test/endpoint.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
1
4
|
const assert = require('node:assert')
|
|
2
5
|
|
|
3
6
|
const mock = require('mock-require')
|
|
@@ -17,6 +20,20 @@ describe('endpoint', () => {
|
|
|
17
20
|
assert.deepEqual(endpoint(25), { host: '::0', port: 25 })
|
|
18
21
|
})
|
|
19
22
|
|
|
23
|
+
it('Unbracketed IPv6 host uses default port', () => {
|
|
24
|
+
assert.deepEqual(endpoint('::0', 25), {
|
|
25
|
+
host: '::0',
|
|
26
|
+
port: 25,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('Unbracketed IPv6 host:port parses correctly (PR #3552 compatibility)', () => {
|
|
31
|
+
assert.deepEqual(endpoint('::0:25'), {
|
|
32
|
+
host: '::0',
|
|
33
|
+
port: 25,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
20
37
|
it('Default port if only host', () => {
|
|
21
38
|
assert.deepEqual(endpoint('10.0.0.3', 42), {
|
|
22
39
|
host: '10.0.0.3',
|
|
@@ -24,6 +41,13 @@ describe('endpoint', () => {
|
|
|
24
41
|
})
|
|
25
42
|
})
|
|
26
43
|
|
|
44
|
+
it('Bracketed IPv6 host is normalized to lowercase', () => {
|
|
45
|
+
assert.deepEqual(endpoint('[ABCD::EF01]:2525'), {
|
|
46
|
+
host: 'abcd::ef01',
|
|
47
|
+
port: 2525,
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
27
51
|
it('Unix socket', () => {
|
|
28
52
|
assert.deepEqual(endpoint('/foo/bar.sock'), {
|
|
29
53
|
path: '/foo/bar.sock',
|
|
@@ -36,10 +60,16 @@ describe('endpoint', () => {
|
|
|
36
60
|
mode: '770',
|
|
37
61
|
})
|
|
38
62
|
})
|
|
63
|
+
|
|
64
|
+
it('Invalid unbracketed IPv6 host with non-numeric tail returns Error', () => {
|
|
65
|
+
const ep = endpoint('::0:port')
|
|
66
|
+
assert.equal(ep instanceof Error, true)
|
|
67
|
+
assert.match(ep.message, /Invalid socket address/)
|
|
68
|
+
})
|
|
39
69
|
})
|
|
40
70
|
|
|
41
71
|
describe('bind()', () => {
|
|
42
|
-
beforeEach((
|
|
72
|
+
beforeEach(() => {
|
|
43
73
|
// Mock filesystem and log server + fs method calls
|
|
44
74
|
const modes = (this.modes = {})
|
|
45
75
|
const log = (this.log = [])
|
|
@@ -63,12 +93,10 @@ describe('endpoint', () => {
|
|
|
63
93
|
|
|
64
94
|
mock('node:fs/promises', this.mockfs)
|
|
65
95
|
this.endpoint = mock.reRequire('../endpoint')
|
|
66
|
-
done()
|
|
67
96
|
})
|
|
68
97
|
|
|
69
|
-
afterEach((
|
|
98
|
+
afterEach(() => {
|
|
70
99
|
mock.stop('node:fs/promises')
|
|
71
|
-
done()
|
|
72
100
|
})
|
|
73
101
|
|
|
74
102
|
it('IP socket', async () => {
|
package/test/host_pool.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const { describe, it } = require('node:test')
|
|
4
|
+
const assert = require('node:assert/strict')
|
|
4
5
|
|
|
5
6
|
const HostPool = require('../host_pool')
|
|
6
7
|
|
|
7
8
|
describe('HostPool', () => {
|
|
8
|
-
it('get a host', (
|
|
9
|
+
it('get a host', () => {
|
|
9
10
|
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222')
|
|
10
11
|
const host = pool.get_host()
|
|
11
12
|
|
|
12
13
|
assert.ok(/\d\.\d\.\d\.\d/.test(host.host), `'${host.host}' looks like a IP`)
|
|
13
14
|
assert.ok(/\d\d\d\d/.test(host.port), `'${host.port}' looks like a port`)
|
|
14
|
-
done()
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
it('uses all the list', (
|
|
17
|
+
it('uses all the list', () => {
|
|
18
18
|
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222')
|
|
19
19
|
|
|
20
20
|
const host1 = pool.get_host()
|
|
@@ -24,10 +24,9 @@ describe('HostPool', () => {
|
|
|
24
24
|
assert.notEqual(host1.host, host2.host)
|
|
25
25
|
assert.notEqual(host3.host, host2.host)
|
|
26
26
|
assert.equal(host3.host, host1.host)
|
|
27
|
-
done()
|
|
28
27
|
})
|
|
29
28
|
|
|
30
|
-
it('default port 25', (
|
|
29
|
+
it('default port 25', () => {
|
|
31
30
|
const pool = new HostPool('1.1.1.1, 2.2.2.2')
|
|
32
31
|
|
|
33
32
|
const host1 = pool.get_host()
|
|
@@ -35,11 +34,24 @@ describe('HostPool', () => {
|
|
|
35
34
|
|
|
36
35
|
assert.equal(host1.port, 25, `is port 25: ${host1.port}`)
|
|
37
36
|
assert.equal(host2.port, 25, `is port 25: ${host2.port}`)
|
|
38
|
-
done()
|
|
39
37
|
})
|
|
40
38
|
|
|
41
|
-
it('dead host', (
|
|
42
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222')
|
|
39
|
+
it('dead host', () => {
|
|
40
|
+
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222', 0.001)
|
|
41
|
+
pool.get_socket = () => ({
|
|
42
|
+
pretendTimeout: () => {},
|
|
43
|
+
setTimeout(ms, cb) {
|
|
44
|
+
this.pretendTimeout = cb
|
|
45
|
+
},
|
|
46
|
+
listeners: {},
|
|
47
|
+
on(ev, cb) {
|
|
48
|
+
this.listeners[ev] = cb
|
|
49
|
+
},
|
|
50
|
+
connect(port, host, cb) {
|
|
51
|
+
cb()
|
|
52
|
+
}, // immediately "connects" to stop retry loop
|
|
53
|
+
destroy() {},
|
|
54
|
+
})
|
|
43
55
|
|
|
44
56
|
pool.failed('1.1.1.1', '1111')
|
|
45
57
|
|
|
@@ -51,17 +63,30 @@ describe('HostPool', () => {
|
|
|
51
63
|
assert.equal(host.host, '2.2.2.2', 'dead host is not returned')
|
|
52
64
|
host = pool.get_host()
|
|
53
65
|
assert.equal(host.host, '2.2.2.2', 'dead host is not returned')
|
|
54
|
-
done()
|
|
55
66
|
})
|
|
56
67
|
|
|
57
68
|
// if they're *all* dead, we return a host to try anyway, to keep from
|
|
58
69
|
// accidentally DOS'ing ourselves if there's a transient but widespread
|
|
59
70
|
// network outage
|
|
60
|
-
it("they're all dead", (
|
|
71
|
+
it("they're all dead", () => {
|
|
61
72
|
let host1
|
|
62
73
|
let host2
|
|
63
74
|
|
|
64
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222')
|
|
75
|
+
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222', 0.001)
|
|
76
|
+
pool.get_socket = () => ({
|
|
77
|
+
pretendTimeout: () => {},
|
|
78
|
+
setTimeout(ms, cb) {
|
|
79
|
+
this.pretendTimeout = cb
|
|
80
|
+
},
|
|
81
|
+
listeners: {},
|
|
82
|
+
on(ev, cb) {
|
|
83
|
+
this.listeners[ev] = cb
|
|
84
|
+
},
|
|
85
|
+
connect(port, host, cb) {
|
|
86
|
+
cb()
|
|
87
|
+
}, // immediately "connects" to stop retry loop
|
|
88
|
+
destroy() {},
|
|
89
|
+
})
|
|
65
90
|
|
|
66
91
|
host1 = pool.get_host()
|
|
67
92
|
|
|
@@ -79,13 +104,12 @@ describe('HostPool', () => {
|
|
|
79
104
|
host2 = pool.get_host()
|
|
80
105
|
assert.ok(host2, "if they're all dead, try one anyway")
|
|
81
106
|
assert.notEqual(host1.host, host2.host, 'rotation continues')
|
|
82
|
-
done()
|
|
83
107
|
})
|
|
84
108
|
|
|
85
109
|
// after .01 secs the timer to retry the dead host will fire, and then
|
|
86
110
|
// we connect using this mock socket, whose "connect" always succeeds
|
|
87
111
|
// so the code brings the dead host back to life
|
|
88
|
-
it('host dead checking timer', (
|
|
112
|
+
it('host dead checking timer', async () => {
|
|
89
113
|
let num_reqs = 0
|
|
90
114
|
const MockSocket = function MockSocket(pool) {
|
|
91
115
|
// these are the methods called from probe_dead_host
|
|
@@ -141,22 +165,24 @@ describe('HostPool', () => {
|
|
|
141
165
|
|
|
142
166
|
// probe_dead_host() will hit two failures and one success (based on
|
|
143
167
|
// num_reqs above). So we wait at least 10s for that to happen:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
clearInterval(interval)
|
|
171
|
+
reject(new Error('probe_dead_host failed'))
|
|
172
|
+
}, 10 * 1000)
|
|
173
|
+
|
|
174
|
+
const interval = setInterval(
|
|
175
|
+
() => {
|
|
176
|
+
if (!pool.dead_hosts['1.1.1.1:1111']) {
|
|
177
|
+
clearTimeout(timer)
|
|
178
|
+
clearInterval(interval)
|
|
179
|
+
resolve()
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
retry_secs * 1000 * 3,
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
assert.ok(true, 'timer un-deaded it')
|
|
161
187
|
})
|
|
162
188
|
})
|