Haraka 3.1.5 → 3.1.7
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} +54 -3
- package/CONTRIBUTORS.md +26 -26
- package/Plugins.md +99 -99
- package/README.md +68 -93
- package/SECURITY.md +178 -0
- package/bin/haraka +7 -14
- package/config/plugins +0 -3
- package/config/smtp_forward.ini +10 -0
- package/config/smtp_proxy.ini +10 -0
- package/connection.js +25 -8
- 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/queue/smtp_forward.md +19 -3
- package/docs/plugins/queue/smtp_proxy.md +10 -2
- 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/haraka.js +1 -1
- package/outbound/hmail.js +42 -41
- package/outbound/index.js +7 -4
- package/outbound/tls.js +2 -43
- package/package.json +51 -61
- package/plugins/auth/auth_base.js +9 -3
- package/plugins/auth/auth_proxy.js +14 -11
- package/plugins/block_me.js +4 -2
- package/plugins/prevent_credential_leaks.js +3 -1
- package/plugins/process_title.js +6 -6
- package/plugins/queue/qmail-queue.js +15 -19
- package/plugins/queue/smtp_forward.js +12 -4
- package/plugins/queue/smtp_proxy.js +14 -3
- package/plugins/tls.js +13 -5
- package/plugins/xclient.js +3 -1
- package/server.js +22 -10
- package/smtp_client.js +20 -11
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/connection.js +258 -0
- package/test/endpoint.js +27 -0
- package/test/outbound/bounce_net_errors.js +3 -2
- 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_bridge.js +80 -0
- package/test/plugins/auth/flat_file.js +128 -0
- package/test/plugins/block_me.js +157 -0
- package/test/plugins/data.signatures.js +114 -0
- package/test/plugins/delay_deny.js +263 -0
- package/test/plugins/prevent_credential_leaks.js +178 -0
- package/test/plugins/process_title.js +135 -0
- package/test/plugins/queue/deliver.js +99 -0
- package/test/plugins/queue/discard.js +79 -0
- package/test/plugins/queue/lmtp.js +138 -0
- package/test/plugins/queue/qmail-queue.js +99 -0
- package/test/plugins/queue/quarantine.js +81 -0
- package/test/plugins/queue/smtp_bridge.js +154 -0
- package/test/plugins/queue/smtp_forward.js +42 -6
- package/test/plugins/queue/smtp_proxy.js +139 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/tarpit.js +91 -0
- package/test/plugins/tls.js +25 -0
- package/test/plugins/toobusy.js +21 -0
- package/test/plugins/xclient.js +14 -0
- package/test/server.js +231 -0
- package/test/smtp_client.js +45 -12
- package/test/tls_socket.js +220 -0
- package/tls_socket.js +52 -2
package/test/smtp_client.js
CHANGED
|
@@ -436,18 +436,18 @@ describe('SMTPClient closed() handler', () => {
|
|
|
436
436
|
})
|
|
437
437
|
})
|
|
438
438
|
|
|
439
|
-
// ───
|
|
439
|
+
// ─── load_tls_options ──────────────────────────────────────────────────────────
|
|
440
440
|
|
|
441
|
-
describe('SMTPClient#
|
|
441
|
+
describe('SMTPClient#load_tls_options', () => {
|
|
442
442
|
it('sets tls_options with servername equal to host', () => {
|
|
443
443
|
const client = makeClient()
|
|
444
|
-
client.
|
|
444
|
+
client.load_tls_options()
|
|
445
445
|
assert.equal(client.tls_options.servername, 'mx.example.com')
|
|
446
446
|
})
|
|
447
447
|
|
|
448
448
|
it('merges additional opts into tls_options', () => {
|
|
449
449
|
const client = makeClient()
|
|
450
|
-
client.
|
|
450
|
+
client.load_tls_options({ key: Buffer.from('secret'), rejectUnauthorized: false })
|
|
451
451
|
assert.equal(client.tls_options.servername, 'mx.example.com')
|
|
452
452
|
assert.equal(client.tls_options.rejectUnauthorized, false)
|
|
453
453
|
assert.ok(Buffer.isBuffer(client.tls_options.key))
|
|
@@ -728,10 +728,8 @@ describe('smtp_client.onCapabilitiesOutbound', () => {
|
|
|
728
728
|
|
|
729
729
|
it('skips STARTTLS when host is in no_tls_hosts ban list', () => {
|
|
730
730
|
client.response = ['STARTTLS']
|
|
731
|
-
//
|
|
732
|
-
// (a known quirk) — set both to exercise the branch
|
|
731
|
+
// no_tls_hosts is read from tls_options consistently
|
|
733
732
|
client.tls_options = { no_tls_hosts: ['10.0.0.0/8'] }
|
|
734
|
-
client.tls_config = { no_tls_hosts: ['10.0.0.0/8'] }
|
|
735
733
|
client.remote_ip = '10.0.0.1'
|
|
736
734
|
const conn = makeConnection()
|
|
737
735
|
smtp_client_module.onCapabilitiesOutbound(client, false, conn, { enable_tls: true, host: '10.0.0.1' }, () => {})
|
|
@@ -766,6 +764,25 @@ describe('smtp_client.get_client_plugin', () => {
|
|
|
766
764
|
assert.ok(client instanceof SMTPClient)
|
|
767
765
|
})
|
|
768
766
|
|
|
767
|
+
it('emits error (does not throw) when AUTH type is unsupported', async () => {
|
|
768
|
+
const c = {
|
|
769
|
+
host: 'relay.example.com',
|
|
770
|
+
port: 25,
|
|
771
|
+
auth_type: 'login',
|
|
772
|
+
auth_user: 'a',
|
|
773
|
+
auth_pass: 'b',
|
|
774
|
+
}
|
|
775
|
+
const client = await getClientPlugin(c)
|
|
776
|
+
client.auth_capabilities = [] // server advertised no AUTH
|
|
777
|
+
let errMsg
|
|
778
|
+
client.on('error', (m) => {
|
|
779
|
+
errMsg = m
|
|
780
|
+
})
|
|
781
|
+
// pre-fix this threw out of the event loop and crashed the worker
|
|
782
|
+
assert.doesNotThrow(() => client.emit('helo'))
|
|
783
|
+
assert.match(String(errMsg), /not supported by server/)
|
|
784
|
+
})
|
|
785
|
+
|
|
769
786
|
it('merges auth_type / auth_user / auth_pass into c.auth', async () => {
|
|
770
787
|
const c = { host: 'relay.example.com', port: 25, auth_type: 'plain', auth_user: 'alice', auth_pass: 's3cr3t' }
|
|
771
788
|
await getClientPlugin(c)
|
|
@@ -791,6 +808,16 @@ describe('smtp_client.get_client_plugin', () => {
|
|
|
791
808
|
assert.ok(written.some((l) => /EHLO relay\.example\.com/.test(l)))
|
|
792
809
|
})
|
|
793
810
|
|
|
811
|
+
it('redacts AUTH credentials from protocol logs (S4)', async () => {
|
|
812
|
+
const client = await getClientPlugin()
|
|
813
|
+
const logged = []
|
|
814
|
+
conn.logprotocol = (p, msg) => logged.push(msg)
|
|
815
|
+
client.emit('client_protocol', 'AUTH PLAIN AGFsaWNlAHMzY3JldA==')
|
|
816
|
+
assert.equal(logged.length, 1)
|
|
817
|
+
assert.equal(logged[0], 'C: AUTH PLAIN [redacted]')
|
|
818
|
+
assert.ok(!logged[0].includes('AGFsaWNlAHMzY3JldA=='))
|
|
819
|
+
})
|
|
820
|
+
|
|
794
821
|
it('greeting handler sends EHLO with hello.host when xclient is set', async () => {
|
|
795
822
|
const client = await getClientPlugin()
|
|
796
823
|
client.xclient = true
|
|
@@ -850,19 +877,25 @@ describe('smtp_client.get_client_plugin', () => {
|
|
|
850
877
|
assert.ok(written.some((l) => /AUTH PLAIN/.test(l)))
|
|
851
878
|
})
|
|
852
879
|
|
|
853
|
-
//
|
|
880
|
+
// these used to throw out of the event loop (crashing the worker); they must
|
|
881
|
+
// now route through the smtp_client 'error' flow.
|
|
854
882
|
for (const [desc, opts, capabilities, pattern] of [
|
|
855
883
|
['unsupported auth type', { type: 'plain', user: 'u', pass: 'p' }, ['cram-md5'], /not supported by server/],
|
|
856
884
|
['plain auth with no user/pass', { type: 'plain', user: '', pass: '' }, ['plain'], /Must include auth\.user/],
|
|
857
|
-
['cram-md5 (not implemented)', { type: 'cram-md5', user: 'u', pass: 'p' }, ['cram-md5'], /
|
|
885
|
+
['cram-md5 (not implemented)', { type: 'cram-md5', user: 'u', pass: 'p' }, ['cram-md5'], /not implemented/i],
|
|
858
886
|
['unknown auth type', { type: 'gssapi', user: 'u', pass: 'p' }, ['gssapi'], /Unknown AUTH type/],
|
|
859
887
|
]) {
|
|
860
|
-
it(`helo handler
|
|
888
|
+
it(`helo handler emits error (no throw) for ${desc}`, async () => {
|
|
861
889
|
const c = { host: 'relay.example.com', port: 25, auth: opts }
|
|
862
890
|
const client = await getClientPlugin(c)
|
|
863
891
|
client.authenticated = false
|
|
864
892
|
client.auth_capabilities = capabilities
|
|
865
|
-
|
|
893
|
+
let errMsg
|
|
894
|
+
client.on('error', (m) => {
|
|
895
|
+
errMsg = m
|
|
896
|
+
})
|
|
897
|
+
assert.doesNotThrow(() => client.emit('helo'))
|
|
898
|
+
assert.match(String(errMsg), pattern)
|
|
866
899
|
})
|
|
867
900
|
}
|
|
868
901
|
|
|
@@ -1241,7 +1274,7 @@ describe('smtp_client', () => {
|
|
|
1241
1274
|
}
|
|
1242
1275
|
|
|
1243
1276
|
const client = new SMTPClient({ host: 'mx.example.com', port: 25, socket })
|
|
1244
|
-
client.
|
|
1277
|
+
client.load_tls_options({ key: Buffer.from('OutboundTlsKeyLoaded') })
|
|
1245
1278
|
|
|
1246
1279
|
client.command = 'starttls'
|
|
1247
1280
|
cmds.line('250 Hello client.example.com\r\n')
|
package/test/tls_socket.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('node:path')
|
|
|
6
6
|
const net = require('node:net')
|
|
7
7
|
const tls = require('node:tls')
|
|
8
8
|
const fs = require('node:fs')
|
|
9
|
+
const { EventEmitter } = require('node:events')
|
|
9
10
|
|
|
10
11
|
// Mock dependencies before requiring the target
|
|
11
12
|
const mock = require('node:test').mock
|
|
@@ -95,10 +96,229 @@ test('tls_socket', async (t) => {
|
|
|
95
96
|
await new Promise((resolve) => server.close(resolve))
|
|
96
97
|
}
|
|
97
98
|
})
|
|
99
|
+
|
|
100
|
+
await t.test('second error handler does not crash when first handler removes all listeners', () => {
|
|
101
|
+
// Regression test for issue #3553
|
|
102
|
+
const originalNetConnect = net.connect
|
|
103
|
+
const originalTlsConnect = tls.connect
|
|
104
|
+
const originalTlsValid = tls_socket.tls_valid
|
|
105
|
+
|
|
106
|
+
const fakeCrypto = new EventEmitter()
|
|
107
|
+
fakeCrypto.writable = true
|
|
108
|
+
fakeCrypto.removeAllListeners = EventEmitter.prototype.removeAllListeners
|
|
109
|
+
fakeCrypto.setTimeout = () => {}
|
|
110
|
+
fakeCrypto.setKeepAlive = () => {}
|
|
111
|
+
|
|
112
|
+
let capturedCleartext
|
|
113
|
+
net.connect = () => fakeCrypto
|
|
114
|
+
tls.connect = () => {
|
|
115
|
+
capturedCleartext = new EventEmitter()
|
|
116
|
+
capturedCleartext.writable = true
|
|
117
|
+
capturedCleartext.setTimeout = () => {}
|
|
118
|
+
capturedCleartext.setKeepAlive = () => {}
|
|
119
|
+
return capturedCleartext
|
|
120
|
+
}
|
|
121
|
+
tls_socket.tls_valid = false
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const socket = tls_socket.connect({ host: 'bad-tls.example.com', port: 25 })
|
|
125
|
+
|
|
126
|
+
// Simulate what release_client does: strip all listeners on first error
|
|
127
|
+
socket.once('error', () => socket.removeAllListeners())
|
|
128
|
+
|
|
129
|
+
socket.upgrade({}, () => {})
|
|
130
|
+
|
|
131
|
+
// capturedCleartext now has two 'error' handlers (on from upgrade, once from attach).
|
|
132
|
+
// Emitting error must NOT throw even though the first handler removes all
|
|
133
|
+
// listeners from the outer socket before the second fires.
|
|
134
|
+
const tlsError = new Error('dh key too small')
|
|
135
|
+
assert.doesNotThrow(() => capturedCleartext.emit('error', tlsError))
|
|
136
|
+
} finally {
|
|
137
|
+
net.connect = originalNetConnect
|
|
138
|
+
tls.connect = originalTlsConnect
|
|
139
|
+
tls_socket.tls_valid = originalTlsValid
|
|
140
|
+
}
|
|
141
|
+
})
|
|
98
142
|
})
|
|
99
143
|
|
|
100
144
|
await t.test('getSocketOpts', async (t) => {
|
|
101
145
|
// Exercise the typo path (would requires failing config.getDir)
|
|
102
146
|
assert.strictEqual(typeof tls_socket.getSocketOpts, 'function')
|
|
103
147
|
})
|
|
148
|
+
|
|
149
|
+
await t.test('getSocketOpts handles missing tls dir', async () => {
|
|
150
|
+
const originalGetCertsDir = tls_socket.get_certs_dir
|
|
151
|
+
tls_socket.get_certs_dir = async () => {
|
|
152
|
+
const err = new Error('missing')
|
|
153
|
+
err.code = 'ENOENT'
|
|
154
|
+
throw err
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const opts = await tls_socket.getSocketOpts('*')
|
|
158
|
+
assert.ok(opts)
|
|
159
|
+
} finally {
|
|
160
|
+
tls_socket.get_certs_dir = originalGetCertsDir
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await t.test('connect upgrade applies mutual auth cert and timeout/keepalive', async () => {
|
|
165
|
+
const originalNetConnect = net.connect
|
|
166
|
+
const originalTlsConnect = tls.connect
|
|
167
|
+
const originalTlsValid = tls_socket.tls_valid
|
|
168
|
+
const originalCfg = tls_socket.cfg
|
|
169
|
+
const originalCertMap = {
|
|
170
|
+
default: tls_socket.certsByHost['*'],
|
|
171
|
+
host: tls_socket.certsByHost['client-cert.example'],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fakeSocket = new EventEmitter()
|
|
175
|
+
fakeSocket.remotePort = 2525
|
|
176
|
+
fakeSocket.remoteAddress = '127.0.0.1'
|
|
177
|
+
fakeSocket.localPort = 25
|
|
178
|
+
fakeSocket.localAddress = '127.0.0.1'
|
|
179
|
+
fakeSocket.writable = true
|
|
180
|
+
fakeSocket.removeAllListeners = EventEmitter.prototype.removeAllListeners
|
|
181
|
+
fakeSocket.setTimeout = () => {}
|
|
182
|
+
fakeSocket.setKeepAlive = () => {}
|
|
183
|
+
|
|
184
|
+
let capturedOptions
|
|
185
|
+
let timeoutSeen = null
|
|
186
|
+
let keepaliveSeen = null
|
|
187
|
+
|
|
188
|
+
net.connect = () => fakeSocket
|
|
189
|
+
tls.connect = (options) => {
|
|
190
|
+
capturedOptions = options
|
|
191
|
+
const clear = new EventEmitter()
|
|
192
|
+
clear.writable = true
|
|
193
|
+
clear.getCipher = () => ({ name: 'TLS_AES_256_GCM_SHA384' })
|
|
194
|
+
clear.getProtocol = () => 'TLSv1.3'
|
|
195
|
+
clear.getPeerCertificate = () => ({})
|
|
196
|
+
clear.setTimeout = (ms) => {
|
|
197
|
+
timeoutSeen = ms
|
|
198
|
+
}
|
|
199
|
+
clear.setKeepAlive = (value) => {
|
|
200
|
+
keepaliveSeen = value
|
|
201
|
+
}
|
|
202
|
+
process.nextTick(() => clear.emit('secureConnect'))
|
|
203
|
+
return clear
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
tls_socket.tls_valid = true
|
|
207
|
+
tls_socket.cfg = {
|
|
208
|
+
mutual_auth_hosts: { 'mx.example.com': 'client-cert.example' },
|
|
209
|
+
mutual_auth_hosts_exclude: {},
|
|
210
|
+
main: { mutual_tls: false },
|
|
211
|
+
}
|
|
212
|
+
tls_socket.certsByHost['*'] = { key: 'default-key', cert: 'default-cert' }
|
|
213
|
+
tls_socket.certsByHost['client-cert.example'] = { key: 'host-key', cert: 'host-cert' }
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const socket = tls_socket.connect({ host: 'mx.example.com', port: 25 })
|
|
217
|
+
socket.setTimeout(3210)
|
|
218
|
+
socket.setKeepAlive(true)
|
|
219
|
+
|
|
220
|
+
await new Promise((resolve) => {
|
|
221
|
+
socket.upgrade({ rejectUnauthorized: false }, () => resolve())
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
assert.equal(capturedOptions.key, 'host-key')
|
|
225
|
+
assert.equal(capturedOptions.cert, 'host-cert')
|
|
226
|
+
assert.equal(capturedOptions.socket, fakeSocket)
|
|
227
|
+
assert.equal(timeoutSeen, 3210)
|
|
228
|
+
assert.equal(keepaliveSeen, true)
|
|
229
|
+
} finally {
|
|
230
|
+
net.connect = originalNetConnect
|
|
231
|
+
tls.connect = originalTlsConnect
|
|
232
|
+
tls_socket.tls_valid = originalTlsValid
|
|
233
|
+
tls_socket.cfg = originalCfg
|
|
234
|
+
tls_socket.certsByHost['*'] = originalCertMap.default
|
|
235
|
+
if (originalCertMap.host === undefined) {
|
|
236
|
+
delete tls_socket.certsByHost['client-cert.example']
|
|
237
|
+
} else {
|
|
238
|
+
tls_socket.certsByHost['client-cert.example'] = originalCertMap.host
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
await t.test('load_plugin_tls_options', async (t) => {
|
|
244
|
+
// Point haraka-config at test/config so tls.ini fixtures load.
|
|
245
|
+
const origConfig = tls_socket.config
|
|
246
|
+
const origCfg = tls_socket.cfg
|
|
247
|
+
const test_config = require('haraka-config').module_config(path.resolve(__dirname))
|
|
248
|
+
|
|
249
|
+
t.beforeEach(() => {
|
|
250
|
+
tls_socket.config = test_config
|
|
251
|
+
tls_socket.cfg = undefined // bust load_tls_ini cache between cases
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
t.after(() => {
|
|
255
|
+
tls_socket.config = origConfig
|
|
256
|
+
tls_socket.cfg = origCfg
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
await t.test('inherits tls.ini [main] when plugin cfg is empty', () => {
|
|
260
|
+
const opts = tls_socket.load_plugin_tls_options({})
|
|
261
|
+
// From test/config/tls.ini [main]
|
|
262
|
+
assert.equal(opts.rejectUnauthorized, false)
|
|
263
|
+
assert.equal(opts.minVersion, 'TLSv1')
|
|
264
|
+
assert.equal(opts.honorCipherOrder, true)
|
|
265
|
+
assert.ok(opts.ciphers && opts.ciphers.length)
|
|
266
|
+
assert.ok(Buffer.isBuffer(opts.key), 'key resolved to Buffer')
|
|
267
|
+
assert.ok(Buffer.isBuffer(opts.cert), 'cert resolved to Buffer')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
await t.test('plugin cfg overrides [main]', () => {
|
|
271
|
+
const opts = tls_socket.load_plugin_tls_options({
|
|
272
|
+
rejectUnauthorized: true,
|
|
273
|
+
minVersion: 'TLSv1.3',
|
|
274
|
+
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384',
|
|
275
|
+
})
|
|
276
|
+
assert.equal(opts.rejectUnauthorized, true)
|
|
277
|
+
assert.equal(opts.minVersion, 'TLSv1.3')
|
|
278
|
+
assert.equal(opts.ciphers, 'ECDHE-RSA-AES256-GCM-SHA384')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
await t.test('resolves key/cert/dhparam file refs to Buffers', () => {
|
|
282
|
+
const opts = tls_socket.load_plugin_tls_options({
|
|
283
|
+
key: 'outbound_tls_key.pem',
|
|
284
|
+
cert: 'outbound_tls_cert.pem',
|
|
285
|
+
dhparam: 'dhparams.pem',
|
|
286
|
+
})
|
|
287
|
+
assert.ok(Buffer.isBuffer(opts.key) && opts.key.length > 0)
|
|
288
|
+
assert.ok(Buffer.isBuffer(opts.cert) && opts.cert.length > 0)
|
|
289
|
+
assert.ok(Buffer.isBuffer(opts.dhparam) && opts.dhparam.length > 0)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
await t.test('drops missing dhparam rather than leaving null', () => {
|
|
293
|
+
const opts = tls_socket.load_plugin_tls_options({
|
|
294
|
+
dhparam: 'does_not_exist.pem',
|
|
295
|
+
})
|
|
296
|
+
assert.equal(opts.dhparam, undefined)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
await t.test('normalises no_tls_hosts / force_tls_hosts to arrays', () => {
|
|
300
|
+
const opts = tls_socket.load_plugin_tls_options({
|
|
301
|
+
no_tls_hosts: '10.0.0.5',
|
|
302
|
+
force_tls_hosts: ['a.example.com', 'b.example.com'],
|
|
303
|
+
})
|
|
304
|
+
assert.deepEqual(opts.no_tls_hosts, ['10.0.0.5'])
|
|
305
|
+
assert.deepEqual(opts.force_tls_hosts, ['a.example.com', 'b.example.com'])
|
|
306
|
+
|
|
307
|
+
const opts2 = tls_socket.load_plugin_tls_options({})
|
|
308
|
+
assert.deepEqual(opts2.no_tls_hosts, [])
|
|
309
|
+
assert.deepEqual(opts2.force_tls_hosts, [])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
await t.test('does not set servername', () => {
|
|
313
|
+
const opts = tls_socket.load_plugin_tls_options({})
|
|
314
|
+
assert.equal(opts.servername, undefined)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
await t.test('does not mutate the input plugin cfg', () => {
|
|
318
|
+
const input = { rejectUnauthorized: true, no_tls_hosts: '10.0.0.5' }
|
|
319
|
+
const before = JSON.stringify(input)
|
|
320
|
+
tls_socket.load_plugin_tls_options(input)
|
|
321
|
+
assert.equal(JSON.stringify(input), before)
|
|
322
|
+
})
|
|
323
|
+
})
|
|
104
324
|
})
|
package/tls_socket.js
CHANGED
|
@@ -76,7 +76,7 @@ class pluggableStream extends stream.Stream {
|
|
|
76
76
|
this.targetsocket.once('error', (exception) => {
|
|
77
77
|
this.writable = this.targetsocket.writable
|
|
78
78
|
exception.source = 'tls'
|
|
79
|
-
this.emit('error', exception)
|
|
79
|
+
if (this.listenerCount('error') > 0) this.emit('error', exception)
|
|
80
80
|
})
|
|
81
81
|
this.targetsocket.on('timeout', () => {
|
|
82
82
|
this.emit('timeout')
|
|
@@ -225,7 +225,7 @@ exports.load_tls_ini = (opts) => {
|
|
|
225
225
|
|
|
226
226
|
if (ocsp === undefined && cfg.main.requestOCSP) {
|
|
227
227
|
try {
|
|
228
|
-
ocsp = require('ocsp')
|
|
228
|
+
ocsp = require('@haraka/ocsp')
|
|
229
229
|
log.debug('ocsp loaded')
|
|
230
230
|
ocspCache = new ocsp.Cache()
|
|
231
231
|
} catch (ignore) {
|
|
@@ -251,6 +251,56 @@ exports.load_tls_ini = (opts) => {
|
|
|
251
251
|
return cfg
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// Build a client tls_options, merges a consumers own [tls] section
|
|
255
|
+
// over tls.ini [main].
|
|
256
|
+
exports.load_plugin_tls_options = (plugin_tls_cfg = {}) => {
|
|
257
|
+
const tls_cfg = exports.load_tls_ini({ role: 'client' })
|
|
258
|
+
const cfg = JSON.parse(JSON.stringify(plugin_tls_cfg))
|
|
259
|
+
|
|
260
|
+
// Inheritance from tls.ini [main] deliberately omits no_tls_hosts: the
|
|
261
|
+
// [main].no_tls_hosts list is documented as inbound-only; outbound and
|
|
262
|
+
// queue plugins should opt in explicitly via their own section.
|
|
263
|
+
const inheritable_opts = [
|
|
264
|
+
'key',
|
|
265
|
+
'cert',
|
|
266
|
+
'ciphers',
|
|
267
|
+
'minVersion',
|
|
268
|
+
'dhparam',
|
|
269
|
+
'requestCert',
|
|
270
|
+
'honorCipherOrder',
|
|
271
|
+
'rejectUnauthorized',
|
|
272
|
+
'force_tls_hosts',
|
|
273
|
+
]
|
|
274
|
+
for (const opt of inheritable_opts) {
|
|
275
|
+
if (cfg[opt] !== undefined) continue // set in plugin [tls]
|
|
276
|
+
if (tls_cfg.main[opt] === undefined) continue // unset in tls.ini [main]
|
|
277
|
+
cfg[opt] = tls_cfg.main[opt]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Resolve key/cert/dhparam file references to buffers. Drop empty results
|
|
281
|
+
// so we never pass null to tls.connect.
|
|
282
|
+
for (const k of ['key', 'cert', 'dhparam']) {
|
|
283
|
+
if (!cfg[k]) {
|
|
284
|
+
delete cfg[k]
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
const ref = Array.isArray(cfg[k]) ? cfg[k][0] : cfg[k]
|
|
288
|
+
const bin = exports.config.get(ref, 'binary')
|
|
289
|
+
if (bin) cfg[k] = bin
|
|
290
|
+
else delete cfg[k]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const k of ['no_tls_hosts', 'force_tls_hosts']) {
|
|
294
|
+
if (!cfg[k]) {
|
|
295
|
+
cfg[k] = []
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
if (!Array.isArray(cfg[k])) cfg[k] = [cfg[k]]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return cfg
|
|
302
|
+
}
|
|
303
|
+
|
|
254
304
|
exports.applySocketOpts = (name) => {
|
|
255
305
|
// https://nodejs.org/api/tls.html#tls_new_tls_tlssocket_socket_options
|
|
256
306
|
const TLSSocketOptions = [
|