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.
Files changed (79) hide show
  1. package/.prettierignore +1 -1
  2. package/{Changes.md → CHANGELOG.md} +54 -3
  3. package/CONTRIBUTORS.md +26 -26
  4. package/Plugins.md +99 -99
  5. package/README.md +68 -93
  6. package/SECURITY.md +178 -0
  7. package/bin/haraka +7 -14
  8. package/config/plugins +0 -3
  9. package/config/smtp_forward.ini +10 -0
  10. package/config/smtp_proxy.ini +10 -0
  11. package/connection.js +25 -8
  12. package/docs/Connection.md +126 -39
  13. package/docs/CoreConfig.md +92 -74
  14. package/docs/HAProxy.md +41 -25
  15. package/docs/Logging.md +68 -38
  16. package/docs/Outbound.md +124 -179
  17. package/docs/Plugins.md +38 -59
  18. package/docs/Transaction.md +78 -83
  19. package/docs/Tutorial.md +122 -209
  20. package/docs/plugins/aliases.md +1 -141
  21. package/docs/plugins/auth/auth_ldap.md +2 -39
  22. package/docs/plugins/max_unrecognized_commands.md +4 -18
  23. package/docs/plugins/process_title.md +3 -3
  24. package/docs/plugins/queue/smtp_forward.md +19 -3
  25. package/docs/plugins/queue/smtp_proxy.md +10 -2
  26. package/docs/plugins/reseed_rng.md +11 -13
  27. package/docs/plugins/tls.md +7 -7
  28. package/docs/plugins/toobusy.md +10 -4
  29. package/docs/tutorials/SettingUpOutbound.md +40 -48
  30. package/endpoint.js +32 -2
  31. package/haraka.js +1 -1
  32. package/outbound/hmail.js +42 -41
  33. package/outbound/index.js +7 -4
  34. package/outbound/tls.js +2 -43
  35. package/package.json +51 -61
  36. package/plugins/auth/auth_base.js +9 -3
  37. package/plugins/auth/auth_proxy.js +14 -11
  38. package/plugins/block_me.js +4 -2
  39. package/plugins/prevent_credential_leaks.js +3 -1
  40. package/plugins/process_title.js +6 -6
  41. package/plugins/queue/qmail-queue.js +15 -19
  42. package/plugins/queue/smtp_forward.js +12 -4
  43. package/plugins/queue/smtp_proxy.js +14 -3
  44. package/plugins/tls.js +13 -5
  45. package/plugins/xclient.js +3 -1
  46. package/server.js +22 -10
  47. package/smtp_client.js +20 -11
  48. package/test/config/block_me.recipient +1 -0
  49. package/test/config/block_me.senders +1 -0
  50. package/test/connection.js +258 -0
  51. package/test/endpoint.js +27 -0
  52. package/test/outbound/bounce_net_errors.js +3 -2
  53. package/test/outbound/hmail.js +19 -0
  54. package/test/outbound/index.js +189 -0
  55. package/test/outbound/queue.js +92 -0
  56. package/test/plugins/auth/auth_bridge.js +80 -0
  57. package/test/plugins/auth/flat_file.js +128 -0
  58. package/test/plugins/block_me.js +157 -0
  59. package/test/plugins/data.signatures.js +114 -0
  60. package/test/plugins/delay_deny.js +263 -0
  61. package/test/plugins/prevent_credential_leaks.js +178 -0
  62. package/test/plugins/process_title.js +135 -0
  63. package/test/plugins/queue/deliver.js +99 -0
  64. package/test/plugins/queue/discard.js +79 -0
  65. package/test/plugins/queue/lmtp.js +138 -0
  66. package/test/plugins/queue/qmail-queue.js +99 -0
  67. package/test/plugins/queue/quarantine.js +81 -0
  68. package/test/plugins/queue/smtp_bridge.js +154 -0
  69. package/test/plugins/queue/smtp_forward.js +42 -6
  70. package/test/plugins/queue/smtp_proxy.js +139 -0
  71. package/test/plugins/reseed_rng.js +34 -0
  72. package/test/plugins/tarpit.js +91 -0
  73. package/test/plugins/tls.js +25 -0
  74. package/test/plugins/toobusy.js +21 -0
  75. package/test/plugins/xclient.js +14 -0
  76. package/test/server.js +231 -0
  77. package/test/smtp_client.js +45 -12
  78. package/test/tls_socket.js +220 -0
  79. package/tls_socket.js +52 -2
@@ -436,18 +436,18 @@ describe('SMTPClient closed() handler', () => {
436
436
  })
437
437
  })
438
438
 
439
- // ─── load_tls_config ──────────────────────────────────────────────────────────
439
+ // ─── load_tls_options ──────────────────────────────────────────────────────────
440
440
 
441
- describe('SMTPClient#load_tls_config', () => {
441
+ describe('SMTPClient#load_tls_options', () => {
442
442
  it('sets tls_options with servername equal to host', () => {
443
443
  const client = makeClient()
444
- client.load_tls_config()
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.load_tls_config({ key: Buffer.from('secret'), rejectUnauthorized: false })
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
- // Note: the code checks tls_options.no_tls_hosts but reads tls_config.no_tls_hosts
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
- // Table-drive the helo-throws cases
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'], /Not implemented/],
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 throws for ${desc}`, async () => {
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
- assert.throws(() => client.emit('helo'), pattern)
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.load_tls_config({ key: Buffer.from('OutboundTlsKeyLoaded') })
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')
@@ -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 = [