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.
@@ -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 = new RegExp('([-]+BEGIN (?:\\w+ )?PRIVATE KEY[-]+[^-]*[-]+END (?:\\w+ )?PRIVATE KEY[-]+)', 'gm')
162
+ const keyRe = /([-]+BEGIN (?:\w+ )?PRIVATE KEY[-]+[^-]*[-]+END (?:\w+ )?PRIVATE KEY[-]+)/gm
163
163
  res.keys = string.match(keyRe)
164
164
 
165
- const certRe = new RegExp('([-]+BEGIN CERTIFICATE[-]+[^-]*[-]+END CERTIFICATE[-]+)', 'gm')
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(...opensslArgs, '-enddate', '-subject', '-ext', 'subjectAltName')
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(`loading tls.ini`) // from ${this.config.root_path}`);
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 != cfg.cert.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', [...params], { timeout: 1000 })
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
- log.debug(`err: ${data.toString().trim()}`)
460
+ errTxt += data
465
461
  })
466
462
 
467
463
  o.on('close', (code) => {
468
464
  if (code !== 0) {
469
- if (code) console.error(code)
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
- console.error(err.messsage)
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', `${fpResolved}`, '2048'])
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).forEach((key) => {
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 = new tls.connect(options)
686
+ const cleartext = tls.connect(options)
692
687
 
693
688
  pipe(cleartext, cryptoSocket)
694
689
 
695
690
  cleartext.on('error', (err) => {
696
- if (err.reason) log.error(`client TLS error: ${err}`)
691
+ err.source = 'tls'
692
+ socket.emit('error', err)
697
693
  })
698
694
 
699
695
  cleartext.once('secureConnect', () => {