Haraka 3.1.4 → 3.1.5
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/CONTRIBUTORS.md +1 -1
- package/Changes.md +14 -0
- package/package.json +8 -10
- package/plugins/queue/smtp_forward.js +4 -4
- package/run_tests +3 -15
- package/smtp_client.js +8 -6
- package/test/endpoint.js +5 -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/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 +281 -436
- package/test/smtp_client.js +1192 -218
- package/test/tls_socket.js +104 -0
- package/tls_socket.js +16 -20
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('node:test')
|
|
4
|
+
const assert = require('node:assert/strict')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const net = require('node:net')
|
|
7
|
+
const tls = require('node:tls')
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
|
|
10
|
+
// Mock dependencies before requiring the target
|
|
11
|
+
const mock = require('node:test').mock
|
|
12
|
+
|
|
13
|
+
const tls_socket = require('../tls_socket')
|
|
14
|
+
|
|
15
|
+
const TEST_CERT = fs.readFileSync(path.join(__dirname, 'config/tls_cert.pem'))
|
|
16
|
+
const TEST_KEY = fs.readFileSync(path.join(__dirname, 'config/tls_key.pem'))
|
|
17
|
+
|
|
18
|
+
test('tls_socket', async (t) => {
|
|
19
|
+
await t.test('parse_x509', async (t) => {
|
|
20
|
+
await t.test('handles empty string', async () => {
|
|
21
|
+
const res = await tls_socket.parse_x509('')
|
|
22
|
+
assert.deepEqual(res, {})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await t.test('handles null/undefined', async () => {
|
|
26
|
+
const res = await tls_socket.parse_x509(null)
|
|
27
|
+
assert.deepEqual(res, {})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// This would exercise the uninitialized res.names bug if we had a cert string
|
|
31
|
+
// but since it spawns openssl, we'd need to mock spawn or provide a real cert.
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
await t.test('get_rejectUnauthorized', async (t) => {
|
|
35
|
+
await t.test('returns true if rejectUnauthorized is true', () => {
|
|
36
|
+
assert.strictEqual(tls_socket.get_rejectUnauthorized(true, 25, [25]), true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
await t.test('returns true if port is in port_list', () => {
|
|
40
|
+
assert.strictEqual(tls_socket.get_rejectUnauthorized(false, 465, [465]), true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await t.test('returns false if port is not in port_list', () => {
|
|
44
|
+
assert.strictEqual(tls_socket.get_rejectUnauthorized(false, 25, [465]), false)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await t.test('SNICallback', async (t) => {
|
|
49
|
+
await t.test('calls sniDone with default context if servername unknown', (t, done) => {
|
|
50
|
+
// This test requires some setup of ctxByHost which is private to the module
|
|
51
|
+
// but we can test if it's a function
|
|
52
|
+
assert.strictEqual(typeof tls_socket.SNICallback, 'function')
|
|
53
|
+
done()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
await t.test('pluggableStream', async (t) => {
|
|
58
|
+
// This is a class inside the file, but not exported.
|
|
59
|
+
// We can test it via createServer or connect if we mock net.
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await t.test('connect', async (t) => {
|
|
63
|
+
// Exercise the `new tls.connect` bug
|
|
64
|
+
// We can't easily catch the 'new' keyword usage without proxying tls.connect
|
|
65
|
+
assert.strictEqual(typeof tls_socket.connect, 'function')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await t.test('connect upgrade error propagation', async (t) => {
|
|
69
|
+
// Verify that TLS errors during socket.upgrade() are propagated to the outer
|
|
70
|
+
// pluggableStream socket, not silently swallowed.
|
|
71
|
+
// A TLS server that requires a client cert; connecting without one triggers
|
|
72
|
+
// a post-handshake "certificate required" alert (TLSv1.3).
|
|
73
|
+
await t.test('emits error on outer socket when client cert is missing', async () => {
|
|
74
|
+
const server = tls.createServer(
|
|
75
|
+
{ cert: TEST_CERT, key: TEST_KEY, requestCert: true, rejectUnauthorized: true },
|
|
76
|
+
() => {},
|
|
77
|
+
)
|
|
78
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
79
|
+
const { port } = server.address()
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const err = await new Promise((resolve, reject) => {
|
|
83
|
+
const socket = tls_socket.connect({ host: '127.0.0.1', port })
|
|
84
|
+
socket.upgrade({ rejectUnauthorized: false }, () => {})
|
|
85
|
+
socket.on('error', resolve)
|
|
86
|
+
socket.on('close', () => reject(new Error('closed without error')))
|
|
87
|
+
setTimeout(() => reject(new Error('timeout')), 3000)
|
|
88
|
+
})
|
|
89
|
+
assert.ok(
|
|
90
|
+
/certificate required|socket hang up|disconnected/.test(err.message),
|
|
91
|
+
`unexpected error: ${err.message}`,
|
|
92
|
+
)
|
|
93
|
+
assert.equal(err.source, 'tls', 'error.source should be "tls"')
|
|
94
|
+
} finally {
|
|
95
|
+
await new Promise((resolve) => server.close(resolve))
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await t.test('getSocketOpts', async (t) => {
|
|
101
|
+
// Exercise the typo path (would requires failing config.getDir)
|
|
102
|
+
assert.strictEqual(typeof tls_socket.getSocketOpts, 'function')
|
|
103
|
+
})
|
|
104
|
+
})
|
package/tls_socket.js
CHANGED
|
@@ -159,21 +159,16 @@ exports.parse_x509 = async (string) => {
|
|
|
159
159
|
const res = {}
|
|
160
160
|
if (!string) return res
|
|
161
161
|
|
|
162
|
-
const keyRe =
|
|
162
|
+
const keyRe = /([-]+BEGIN (?:\w+ )?PRIVATE KEY[-]+[^-]*[-]+END (?:\w+ )?PRIVATE KEY[-]+)/gm
|
|
163
163
|
res.keys = string.match(keyRe)
|
|
164
164
|
|
|
165
|
-
const certRe =
|
|
165
|
+
const certRe = /([-]+BEGIN CERTIFICATE[-]+[^-]*[-]+END CERTIFICATE[-]+)/gm
|
|
166
166
|
res.chain = string.match(certRe)
|
|
167
167
|
|
|
168
168
|
if (res.chain?.length) {
|
|
169
|
-
const opensslArgs = [res.chain[0], 'x509', '-noout']
|
|
170
|
-
// shush openssl, https://github.com/openssl/openssl/issues/22893
|
|
171
|
-
// if (['darwin','linux','freebsd'].includes(process.platform))
|
|
172
|
-
// opensslArgs.push('-in', '/dev/stdin')
|
|
173
|
-
|
|
174
169
|
// it's cleaner to call openssl with each of -enddate, -subject, etc, but it costs
|
|
175
170
|
// 40-50ms per spawn with node v21 on a M1 MBP
|
|
176
|
-
const raw = await openssl(
|
|
171
|
+
const raw = await openssl(res.chain[0], 'x509', '-noout', '-enddate', '-subject', '-ext', 'subjectAltName')
|
|
177
172
|
if (!raw) return res
|
|
178
173
|
|
|
179
174
|
res.expire = new Date(raw.match(/notAfter=(.* [A-Z]{3})/)[1])
|
|
@@ -191,7 +186,7 @@ exports.parse_x509 = async (string) => {
|
|
|
191
186
|
}
|
|
192
187
|
|
|
193
188
|
exports.load_tls_ini = (opts) => {
|
|
194
|
-
log.info(
|
|
189
|
+
log.info('loading tls.ini')
|
|
195
190
|
|
|
196
191
|
const cfg = exports.config.get(
|
|
197
192
|
'tls.ini',
|
|
@@ -339,7 +334,7 @@ exports.load_default_opts = () => {
|
|
|
339
334
|
if (!Array.isArray(cfg.key)) cfg.key = [cfg.key]
|
|
340
335
|
if (!Array.isArray(cfg.cert)) cfg.cert = [cfg.cert]
|
|
341
336
|
|
|
342
|
-
if (cfg.key.length
|
|
337
|
+
if (cfg.key.length !== cfg.cert.length) {
|
|
343
338
|
log.error(`number of keys (${cfg.key.length}) not equal to certs (${cfg.cert.length}).`)
|
|
344
339
|
}
|
|
345
340
|
|
|
@@ -454,19 +449,20 @@ exports.get_certs_dir = async (tlsDir) => {
|
|
|
454
449
|
function openssl(crt, ...params) {
|
|
455
450
|
return new Promise((resolve) => {
|
|
456
451
|
let crtTxt = ''
|
|
452
|
+
let errTxt = ''
|
|
457
453
|
|
|
458
|
-
const o = spawn('openssl',
|
|
454
|
+
const o = spawn('openssl', params, { timeout: 2000 })
|
|
459
455
|
o.stdout.on('data', (data) => {
|
|
460
456
|
crtTxt += data
|
|
461
457
|
})
|
|
462
458
|
|
|
463
459
|
o.stderr.on('data', (data) => {
|
|
464
|
-
|
|
460
|
+
errTxt += data
|
|
465
461
|
})
|
|
466
462
|
|
|
467
463
|
o.on('close', (code) => {
|
|
468
464
|
if (code !== 0) {
|
|
469
|
-
|
|
465
|
+
log.error(`openssl ${params.join(' ')} failed with code ${code}: ${errTxt.trim()}`)
|
|
470
466
|
}
|
|
471
467
|
resolve(crtTxt)
|
|
472
468
|
})
|
|
@@ -484,8 +480,7 @@ exports.getSocketOpts = async (name) => {
|
|
|
484
480
|
await this.get_certs_dir('tls')
|
|
485
481
|
} catch (err) {
|
|
486
482
|
if (err.code !== 'ENOENT') {
|
|
487
|
-
|
|
488
|
-
log.error(err)
|
|
483
|
+
log.error(err.message)
|
|
489
484
|
}
|
|
490
485
|
}
|
|
491
486
|
|
|
@@ -519,7 +514,7 @@ exports.ensureDhparams = (done) => {
|
|
|
519
514
|
|
|
520
515
|
log.info(`Generating a 2048 bit dhparams file at ${fpResolved}`)
|
|
521
516
|
|
|
522
|
-
const o = spawn('openssl', ['dhparam', '-out',
|
|
517
|
+
const o = spawn('openssl', ['dhparam', '-out', fpResolved, '2048'], { timeout: 30000 })
|
|
523
518
|
o.stdout.on('data', (data) => {
|
|
524
519
|
// normally empty output
|
|
525
520
|
log.debug(data)
|
|
@@ -586,9 +581,9 @@ exports.shutdown = () => {
|
|
|
586
581
|
|
|
587
582
|
function cleanOcspCache() {
|
|
588
583
|
log.debug(`Cleaning ocspCache. How many keys? ${Object.keys(ocspCache.cache).length}`)
|
|
589
|
-
Object.keys(ocspCache.cache)
|
|
584
|
+
for (const key of Object.keys(ocspCache.cache)) {
|
|
590
585
|
clearTimeout(ocspCache.cache[key].timer)
|
|
591
|
-
}
|
|
586
|
+
}
|
|
592
587
|
}
|
|
593
588
|
|
|
594
589
|
exports.certsByHost = certsByHost
|
|
@@ -688,12 +683,13 @@ function connect(conn_options = {}) {
|
|
|
688
683
|
}
|
|
689
684
|
options.socket = cryptoSocket
|
|
690
685
|
|
|
691
|
-
const cleartext =
|
|
686
|
+
const cleartext = tls.connect(options)
|
|
692
687
|
|
|
693
688
|
pipe(cleartext, cryptoSocket)
|
|
694
689
|
|
|
695
690
|
cleartext.on('error', (err) => {
|
|
696
|
-
|
|
691
|
+
err.source = 'tls'
|
|
692
|
+
socket.emit('error', err)
|
|
697
693
|
})
|
|
698
694
|
|
|
699
695
|
cleartext.once('secureConnect', () => {
|