• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

haraka / Haraka / 26427888890

26 May 2026 01:56AM UTC coverage: 73.353% (-0.3%) from 73.64%
26427888890

Pull #3577

github

web-flow
Merge d6883ed79 into c4173efb8
Pull Request #3577: Implicit TLS with Proxy Protocol

1807 of 2349 branches covered (76.93%)

186 of 220 new or added lines in 2 files covered. (84.55%)

93 existing lines in 7 files now uncovered.

8115 of 11063 relevant lines covered (73.35%)

24.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.44
/tls_socket.js
1
'use strict'
14✔
2

14✔
3
const cluster = require('node:cluster')
14✔
4
const net = require('node:net')
14✔
5
const path = require('node:path')
14✔
6
const { spawn } = require('node:child_process')
14✔
7
const stream = require('node:stream')
14✔
8
const tls = require('node:tls')
14✔
9
const util = require('node:util')
14✔
10

14✔
11
// npm packages
14✔
12
exports.config = require('haraka-config') // exported for tests
14✔
13
const Notes = require('haraka-notes')
14✔
14

14✔
15
const log = require('./logger')
14✔
16

14✔
17
const certsByHost = new Notes()
14✔
18
const ctxByHost = {}
14✔
19
let ocsp
14✔
20
let ocspCache
14✔
21

14✔
22
// provides a common socket for attaching
14✔
23
// and detaching from either main socket, or crypto socket
14✔
24
class pluggableStream extends stream.Stream {
14✔
25
    constructor(socket) {
14✔
26
        super()
12✔
27
        this.readable = this.writable = true
12✔
28
        this._timeout = 0
12✔
29
        this._keepalive = false
12✔
30
        this._writeState = true
12✔
31
        this._pending = []
12✔
32
        this._pendingCallbacks = []
12✔
33
        if (socket) this.attach(socket)
12✔
34
    }
12✔
35

14✔
36
    pause() {
14✔
37
        if (this.targetsocket.pause) {
6✔
38
            this.targetsocket.pause()
6✔
39
            this.readable = false
6✔
40
        }
6✔
41
    }
6✔
42

14✔
43
    resume() {
14✔
44
        if (this.targetsocket.resume) {
6✔
45
            this.readable = true
6✔
46
            this.targetsocket.resume()
6✔
47
        }
6✔
48
    }
6✔
49

14✔
50
    attach(socket) {
14✔
51
        this.targetsocket = socket
16✔
52
        this.targetsocket.on('data', (data) => {
16✔
53
            this.emit('data', data)
53✔
54
        })
16✔
55
        this.targetsocket.on('connect', (a, b) => {
16✔
56
            this.emit('connect', a, b)
6✔
57
        })
16✔
58
        this.targetsocket.on('secureConnect', (a, b) => {
16✔
59
            this.emit('secureConnect', a, b)
2✔
60
            this.emit('secure', a, b)
2✔
61
        })
16✔
62
        this.targetsocket.on('secure', (a, b) => {
16✔
63
            this.emit('secure', a, b)
1✔
64
        })
16✔
65
        this.targetsocket.on('end', () => {
16✔
66
            this.writable = this.targetsocket.writable
5✔
67
            this.emit('end')
5✔
68
        })
16✔
69
        this.targetsocket.on('close', (had_error) => {
16✔
70
            this.writable = this.targetsocket.writable
10✔
71
            this.emit('close', had_error)
10✔
72
        })
16✔
73
        this.targetsocket.on('drain', () => {
16✔
74
            this.emit('drain')
×
75
        })
16✔
76
        this.targetsocket.once('error', (exception) => {
16✔
77
            this.writable = this.targetsocket.writable
3✔
78
            exception.source = 'tls'
3✔
79
            if (this.listenerCount('error') > 0) this.emit('error', exception)
3✔
80
        })
16✔
81
        this.targetsocket.on('timeout', () => {
16✔
82
            this.emit('timeout')
×
83
        })
16✔
84
        if (this.targetsocket.remotePort) {
16✔
85
            this.remotePort = this.targetsocket.remotePort
7✔
86
        }
7✔
87
        if (this.targetsocket.remoteAddress) {
16✔
88
            this.remoteAddress = this.targetsocket.remoteAddress
7✔
89
        }
7✔
90
        if (this.targetsocket.localPort) {
16✔
91
            this.localPort = this.targetsocket.localPort
7✔
92
        }
7✔
93
        if (this.targetsocket.localAddress) {
16✔
94
            this.localAddress = this.targetsocket.localAddress
7✔
95
        }
7✔
96
    }
16✔
97

14✔
98
    clean(data) {
14✔
99
        if (this.targetsocket?.removeAllListeners) {
4✔
100
            for (const name of ['data', 'secure', 'secureConnect', 'end', 'close', 'error', 'drain']) {
4✔
101
                this.targetsocket.removeAllListeners(name)
28✔
102
            }
28✔
103
        }
4✔
104
        this.targetsocket = {}
4✔
105
        this.targetsocket.write = () => {}
4✔
106
    }
4✔
107

14✔
108
    write(data, encoding, callback) {
14✔
109
        if (this.targetsocket.write) {
66✔
110
            return this.targetsocket.write(data, encoding, callback)
66✔
111
        }
66✔
112
        return false
×
113
    }
66✔
114

14✔
115
    end(data, encoding) {
14✔
116
        if (this.targetsocket.end) {
5✔
117
            return this.targetsocket.end(data, encoding)
5✔
118
        }
5✔
119
    }
5✔
120

14✔
121
    destroySoon() {
14✔
122
        if (this.targetsocket.destroySoon) {
×
123
            return this.targetsocket.destroySoon()
×
124
        }
×
125
    }
×
126

14✔
127
    destroy() {
14✔
128
        if (this.targetsocket.destroy) {
4✔
129
            return this.targetsocket.destroy()
4✔
130
        }
4✔
131
    }
4✔
132

14✔
133
    setKeepAlive(bool) {
14✔
134
        this._keepalive = bool
5✔
135
        return this.targetsocket.setKeepAlive(bool)
5✔
136
    }
5✔
137

14✔
138
    setNoDelay(/* true||false */) {}
14✔
139

14✔
140
    unref() {
14✔
141
        return this.targetsocket.unref()
×
142
    }
×
143

14✔
144
    setTimeout(timeout) {
14✔
145
        this._timeout = timeout
14✔
146
        return this.targetsocket.setTimeout(timeout)
14✔
147
    }
14✔
148

14✔
149
    isEncrypted() {
14✔
150
        return this.targetsocket.encrypted
×
151
    }
×
152

14✔
153
    isSecure() {
14✔
154
        return this.targetsocket.encrypted && this.targetsocket.authorized
×
155
    }
×
156
}
14✔
157

14✔
158
exports.parse_x509 = async (string) => {
14✔
159
    const res = {}
62✔
160
    if (!string) return res
62✔
161

60✔
162
    const keyRe = /([-]+BEGIN (?:\w+ )?PRIVATE KEY[-]+[^-]*[-]+END (?:\w+ )?PRIVATE KEY[-]+)/gm
60✔
163
    res.keys = string.match(keyRe)
60✔
164

60✔
165
    const certRe = /([-]+BEGIN CERTIFICATE[-]+[^-]*[-]+END CERTIFICATE[-]+)/gm
60✔
166
    res.chain = string.match(certRe)
60✔
167

60✔
168
    if (res.chain?.length) {
62✔
169
        // it's cleaner to call openssl with each of -enddate, -subject, etc, but it costs
40✔
170
        // 40-50ms per spawn with node v21 on a M1 MBP
40✔
171
        const raw = await openssl(res.chain[0], 'x509', '-noout', '-enddate', '-subject', '-ext', 'subjectAltName')
40✔
172
        if (!raw) return res
40!
173

40✔
174
        res.expire = new Date(raw.match(/notAfter=(.* [A-Z]{3})/)[1])
40✔
175

40✔
176
        const match = /CN\s*=\s*([^/\s,]+)/.exec(raw)
40✔
177
        if (match && match[1]) res.names = [match[1]]
40✔
178

40✔
179
        for (let name of Array.from(raw.matchAll(/DNS:([^\s,]+)/gm), (m) => m[0])) {
40!
180
            name = name.replace('DNS:', '')
×
181
            if (!res.names.includes(name)) res.names.push(name)
×
182
        }
×
183
    }
40✔
184

60✔
185
    return res
60✔
186
}
62✔
187

14✔
188
exports.load_tls_ini = (opts) => {
14✔
189
    log.info('loading tls.ini')
147✔
190

147✔
191
    const cfg = exports.config.get(
147✔
192
        'tls.ini',
147✔
193
        {
147✔
194
            booleans: [
147✔
195
                '-redis.disable_for_failed_hosts',
147✔
196

147✔
197
                // wildcards match in any section and are not initialized
147✔
198
                '*.requestCert',
147✔
199
                '*.rejectUnauthorized',
147✔
200
                '*.honorCipherOrder',
147✔
201
                '*.enableOCSPStapling',
147✔
202
                '*.requestOCSP',
147✔
203

147✔
204
                // explicitely declared booleans are initialized
147✔
205
                '+main.requestCert',
147✔
206
                '-main.rejectUnauthorized',
147✔
207
                '+main.honorCipherOrder',
147✔
208
                '-main.requestOCSP',
147✔
209
                '-main.mutual_tls',
147✔
210
            ],
147✔
211
        },
147✔
212
        () => {
147✔
213
            this.load_tls_ini()
×
214
        },
147✔
215
    )
147✔
216

147✔
217
    if (cfg.no_tls_hosts === undefined) cfg.no_tls_hosts = {}
147!
218
    if (cfg.mutual_auth_hosts === undefined) cfg.mutual_auth_hosts = {}
147✔
219
    if (cfg.mutual_auth_hosts_exclude === undefined) cfg.mutual_auth_hosts_exclude = {}
147✔
220

147✔
221
    if (cfg.main.enableOCSPStapling !== undefined) {
147!
222
        log.error('deprecated setting enableOCSPStapling in tls.ini')
×
223
        cfg.main.requestOCSP = cfg.main.enableOCSPStapling
×
224
    }
×
225

147✔
226
    if (ocsp === undefined && cfg.main.requestOCSP) {
147!
227
        try {
×
228
            ocsp = require('@haraka/ocsp')
×
229
            log.debug('ocsp loaded')
×
230
            ocspCache = new ocsp.Cache()
×
231
        } catch (ignore) {
×
232
            log.notice('OCSP Stapling not available.')
×
233
        }
×
234
    }
×
235

147✔
236
    if (cfg.main.requireAuthorized === undefined) {
147✔
237
        cfg.main.requireAuthorized = []
5✔
238
    } else if (!Array.isArray(cfg.main.requireAuthorized)) {
147!
UNCOV
239
        cfg.main.requireAuthorized = [cfg.main.requireAuthorized]
×
UNCOV
240
    }
×
241

147✔
242
    if (!Array.isArray(cfg.main.no_starttls_ports)) cfg.main.no_starttls_ports = []
147✔
243

147✔
244
    this.cfg = cfg
147✔
245

147✔
246
    if (!opts || opts.role === 'server') {
147✔
247
        this.applySocketOpts('*')
9✔
248
        this.load_default_opts()
9✔
249
    }
9✔
250

147✔
251
    return cfg
147✔
252
}
147✔
253

14✔
254
// Build a client tls_options, merges a consumers own [tls] section
14✔
255
// over tls.ini [main].
14✔
256
exports.load_plugin_tls_options = (plugin_tls_cfg = {}) => {
14✔
257
    const tls_cfg = exports.load_tls_ini({ role: 'client' })
137✔
258
    const cfg = JSON.parse(JSON.stringify(plugin_tls_cfg))
137✔
259

137✔
260
    // Inheritance from tls.ini [main] deliberately omits no_tls_hosts: the
137✔
261
    // [main].no_tls_hosts list is documented as inbound-only; outbound and
137✔
262
    // queue plugins should opt in explicitly via their own section.
137✔
263
    const inheritable_opts = [
137✔
264
        'key',
137✔
265
        'cert',
137✔
266
        'ciphers',
137✔
267
        'minVersion',
137✔
268
        'dhparam',
137✔
269
        'requestCert',
137✔
270
        'honorCipherOrder',
137✔
271
        'rejectUnauthorized',
137✔
272
        'force_tls_hosts',
137✔
273
    ]
137✔
274
    for (const opt of inheritable_opts) {
137✔
275
        if (cfg[opt] !== undefined) continue // set in plugin [tls]
1,233✔
276
        if (tls_cfg.main[opt] === undefined) continue // unset in tls.ini [main]
1,224✔
277
        cfg[opt] = tls_cfg.main[opt]
86✔
278
    }
86✔
279

137✔
280
    // Resolve key/cert/dhparam file references to buffers. Drop empty results
137✔
281
    // so we never pass null to tls.connect.
137✔
282
    for (const k of ['key', 'cert', 'dhparam']) {
137✔
283
        if (!cfg[k]) {
411✔
284
            delete cfg[k]
366✔
285
            continue
366✔
286
        }
366✔
287
        const ref = Array.isArray(cfg[k]) ? cfg[k][0] : cfg[k]
411!
288
        const bin = exports.config.get(ref, 'binary')
411✔
289
        if (bin) cfg[k] = bin
411✔
290
        else delete cfg[k]
1✔
291
    }
411✔
292

137✔
293
    for (const k of ['no_tls_hosts', 'force_tls_hosts']) {
137✔
294
        if (!cfg[k]) {
274✔
295
            cfg[k] = []
269✔
296
            continue
269✔
297
        }
269✔
298
        if (!Array.isArray(cfg[k])) cfg[k] = [cfg[k]]
18✔
299
    }
274✔
300

137✔
301
    return cfg
137✔
302
}
137✔
303

14✔
304
exports.applySocketOpts = (name) => {
14✔
305
    // https://nodejs.org/api/tls.html#tls_new_tls_tlssocket_socket_options
49✔
306
    const TLSSocketOptions = [
49✔
307
        // 'server'        // manually added
49✔
308
        'isServer',
49✔
309
        'requestCert',
49✔
310
        'rejectUnauthorized',
49✔
311
        'NPNProtocols',
49✔
312
        'ALPNProtocols',
49✔
313
        'session',
49✔
314
        'requestOCSP',
49✔
315
        'secureContext',
49✔
316
        'SNICallback',
49✔
317
    ]
49✔
318

49✔
319
    // https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
49✔
320
    const createSecureContextOptions = [
49✔
321
        'key',
49✔
322
        'cert',
49✔
323
        'dhparam',
49✔
324
        'pfx',
49✔
325
        'passphrase',
49✔
326
        'ca',
49✔
327
        'crl',
49✔
328
        'ciphers',
49✔
329
        'minVersion',
49✔
330
        'honorCipherOrder',
49✔
331
        'ecdhCurve',
49✔
332
        'secureProtocol',
49✔
333
        'secureOptions',
49✔
334
        'sessionIdContext',
49✔
335
    ]
49✔
336

49✔
337
    for (const opt of [...TLSSocketOptions, ...createSecureContextOptions]) {
49✔
338
        if (this.cfg[name] && this.cfg[name][opt] !== undefined) {
1,127!
339
            // if the setting exists in tls.ini [name]
×
340
            certsByHost.set([name, opt], this.cfg[name][opt])
×
341
        } else if (this.cfg.main[opt] !== undefined) {
1,127✔
342
            // save settings in tls.ini [main] to each CN
431✔
343
            certsByHost.set([name, opt], this.cfg.main[opt])
431✔
344
        } else {
1,127✔
345
            // defaults
696✔
346
            switch (opt) {
696✔
347
                case 'sessionIdContext':
696✔
348
                    certsByHost.set([name, opt], 'haraka')
49✔
349
                    break
49✔
350
                case 'isServer':
696✔
351
                    certsByHost.set([name, opt], true)
49✔
352
                    break
49✔
353
                case 'key':
696✔
354
                    certsByHost.set([name, opt], 'tls_key.pem')
2✔
355
                    break
2✔
356
                case 'cert':
696✔
357
                    certsByHost.set([name, opt], 'tls_cert.pem')
2✔
358
                    break
2✔
359
                case 'dhparam':
696✔
360
                    certsByHost.set([name, opt], 'dhparams.pem')
2✔
361
                    break
2✔
362
                case 'SNICallback':
696✔
363
                    certsByHost.set([name, opt], exports.SNICallback)
49✔
364
                    break
49✔
365
            }
696✔
366
        }
696✔
367
    }
1,127✔
368
}
49✔
369

14✔
370
exports.load_default_opts = () => {
14✔
371
    const cfg = certsByHost['*']
9✔
372

9✔
373
    if (cfg.dhparam && typeof cfg.dhparam === 'string') {
9✔
374
        log.debug(`loading dhparams from ${cfg.dhparam}`)
9✔
375
        certsByHost.set('*.dhparam', this.config.get(cfg.dhparam, 'binary'))
9✔
376
    }
9✔
377

9✔
378
    if (cfg.ca && typeof cfg.ca === 'string') {
9!
379
        log.info(`loading CA certs from ${cfg.ca}`)
×
380
        certsByHost.set('*.ca', this.config.get(cfg.ca, 'binary'))
×
381
    }
×
382

9✔
383
    // make non-array key/cert option into Arrays with one entry
9✔
384
    if (!Array.isArray(cfg.key)) cfg.key = [cfg.key]
9✔
385
    if (!Array.isArray(cfg.cert)) cfg.cert = [cfg.cert]
9✔
386

9✔
387
    if (cfg.key.length !== cfg.cert.length) {
9!
388
        log.error(`number of keys (${cfg.key.length}) not equal to certs (${cfg.cert.length}).`)
×
389
    }
×
390

9✔
391
    // if key file has already been loaded, it'll be a Buffer.
9✔
392
    if (typeof cfg.key[0] === 'string') {
9✔
393
        // turn key/cert file names into actual key/cert binary data
9✔
394
        const asArray = cfg.key.map((keyFileName) => {
9✔
395
            if (!keyFileName) return
9!
396
            const key = this.config.get(keyFileName, 'binary')
9✔
397
            if (!key) {
9✔
398
                log.error(`tls key ${path.join(this.config.root_path, keyFileName)} could not be loaded.`)
2✔
399
            }
2✔
400
            return key
9✔
401
        })
9✔
402
        certsByHost.set('*.key', asArray)
9✔
403
    }
9✔
404

9✔
405
    if (typeof cfg.cert[0] === 'string') {
9✔
406
        const asArray = cfg.cert.map((certFileName) => {
9✔
407
            if (!certFileName) return
9!
408
            const cert = this.config.get(certFileName, 'binary')
9✔
409
            if (!cert) {
9✔
410
                log.error(`tls cert ${path.join(this.config.root_path, certFileName)} could not be loaded.`)
2✔
411
            }
2✔
412
            return cert
9✔
413
        })
9✔
414
        certsByHost.set('*.cert', asArray)
9✔
415
    }
9✔
416

9✔
417
    if (cfg.cert[0] && cfg.key[0]) {
9✔
418
        this.tls_valid = true
7✔
419

7✔
420
        // now that all opts are applied, generate TLS context
7✔
421
        this.ensureDhparams(() => {
7✔
422
            ctxByHost['*'] = tls.createSecureContext(cfg)
7✔
423
        })
7✔
424
    }
7✔
425
}
9✔
426

14✔
427
exports.SNICallback = function (servername, sniDone) {
14✔
428
    log.debug(`SNI servername: ${servername}`)
2✔
429

2✔
430
    sniDone(null, ctxByHost[servername] || ctxByHost['*'])
2✔
431
}
2✔
432

14✔
433
exports.get_certs_dir = async (tlsDir) => {
14✔
434
    const r = {}
20✔
435
    const watcher = async () => {
20✔
436
        exports.get_certs_dir(tlsDir)
×
437
    }
×
438
    const dirOpts = { type: 'binary', watchCb: watcher }
20✔
439

20✔
440
    const files = await this.config.getDir(tlsDir, dirOpts)
20✔
441
    for (const file of files) {
20✔
442
        try {
60✔
443
            r[file.path] = await exports.parse_x509(file.data.toString())
60✔
444
        } catch (err) {
60!
445
            log.debug(err.message)
×
446
        }
×
447
    }
60✔
448

20✔
449
    log.debug(`found ${Object.keys(r).length} files in config/tls`)
20✔
450
    if (Object.keys(r).length === 0) return
20!
451

20✔
452
    const s = {} // certs by name (CN)
20✔
453

20✔
454
    for (const fp in r) {
20✔
455
        if (r[fp].expire && r[fp].expire < new Date()) {
60!
456
            log.error(`${fp} expired on ${r[fp].expire}`)
×
457
        }
×
458

60✔
459
        // a file with a key and no cert, get name from file
60✔
460
        if (!r[fp].names) r[fp].names = [path.parse(fp).name]
60✔
461

60✔
462
        for (let name of r[fp].names) {
60✔
463
            if (name[0] === '_') name = name.replace('_', '*') // windows
60✔
464
            if (s[name] === undefined) s[name] = {}
60✔
465
            if (!s[name].key && r[fp].keys) s[name].key = r[fp].keys[0]
60✔
466
            if (!s[name].cert && r[fp].chain) {
60✔
467
                s[name].cert = r[fp].chain[0]
40✔
468
                s[name].file = fp
40✔
469
            }
40✔
470
        }
60✔
471
    }
60✔
472

20✔
473
    for (const cn in s) {
20✔
474
        if (!s[cn].cert || !s[cn].key) {
40!
475
            delete s[cn]
×
476
            continue
×
477
        }
×
478

40✔
479
        this.applySocketOpts(cn) // from tls.ini
40✔
480
        certsByHost.set([cn, 'cert'], Buffer.from(s[cn].cert))
40✔
481
        certsByHost.set([cn, 'key'], Buffer.from(s[cn].key))
40✔
482
        certsByHost.set([cn, 'dhparam'], certsByHost['*'].dhparam, true)
40✔
483

40✔
484
        // all opts are applied, generate TLS context
40✔
485
        try {
40✔
486
            ctxByHost[cn] = tls.createSecureContext(certsByHost.get([cn]))
40✔
487
        } catch (err) {
40!
488
            log.error(`CN '${cn}' loading got: ${err.message}`)
×
489
            delete ctxByHost[cn]
×
490
            delete certsByHost[cn]
×
491
        }
×
492
    }
40✔
493

20✔
494
    log.info(`found ${Object.keys(s).length} TLS certs in config/tls`)
20✔
495

20✔
496
    return certsByHost // used only by tests
20✔
497
}
20✔
498

14✔
499
function openssl(crt, ...params) {
40✔
500
    return new Promise((resolve) => {
40✔
501
        let crtTxt = ''
40✔
502
        let errTxt = ''
40✔
503

40✔
504
        const o = spawn('openssl', params, { timeout: 2000 })
40✔
505
        o.stdout.on('data', (data) => {
40✔
506
            crtTxt += data
40✔
507
        })
40✔
508

40✔
509
        o.stderr.on('data', (data) => {
40✔
510
            errTxt += data
40✔
511
        })
40✔
512

40✔
513
        o.on('close', (code) => {
40✔
514
            if (code !== 0) {
40!
515
                log.error(`openssl ${params.join(' ')} failed with code ${code}: ${errTxt.trim()}`)
×
516
            }
×
517
            resolve(crtTxt)
40✔
518
        })
40✔
519

40✔
520
        o.stdin.write(crt)
40✔
521
        o.stdin.write('\n')
40✔
522
    })
40✔
523
}
40✔
524

14✔
525
exports.getSocketOpts = async (name) => {
14✔
526
    // startup time, load the config/tls dir
21✔
527
    if (!certsByHost['*']) this.load_tls_ini()
21✔
528

21✔
529
    try {
21✔
530
        await this.get_certs_dir('tls')
21✔
531
    } catch (err) {
21✔
532
        if (err.code !== 'ENOENT') {
1!
533
            log.error(err.message)
×
534
        }
×
535
    }
1✔
536

21✔
537
    return certsByHost[name] || certsByHost['*']
21!
538
}
21✔
539

14✔
540
function pipe(cleartext, socket) {
4✔
541
    cleartext.socket = socket
4✔
542

4✔
543
    function onError(e) {}
4✔
544

4✔
545
    function onClose() {
4✔
546
        socket.removeListener('error', onError)
2✔
547
        socket.removeListener('close', onClose)
2✔
548
    }
2✔
549

4✔
550
    socket.on('error', onError)
4✔
551
    socket.on('close', onClose)
4✔
552
}
4✔
553

14✔
554
exports.ensureDhparams = (done) => {
14✔
555
    // empty/missing dhparams file
7✔
556
    if (certsByHost['*'].dhparam) {
7✔
557
        return done(null, certsByHost['*'].dhparam)
7✔
558
    }
7✔
559

×
560
    if (cluster.isWorker) return // only once, on the master process
×
561

×
562
    const filePath = this.cfg.main.dhparam || 'dhparams.pem'
×
563
    const fpResolved = path.resolve(exports.config.root_path, filePath)
7✔
564

7✔
565
    log.info(`Generating a 2048 bit dhparams file at ${fpResolved}`)
7✔
566

7✔
567
    const o = spawn('openssl', ['dhparam', '-out', fpResolved, '2048'], { timeout: 30000 })
7✔
568
    o.stdout.on('data', (data) => {
7✔
569
        // normally empty output
×
570
        log.debug(data)
×
571
    })
7✔
572

7✔
573
    o.stderr.on('data', (data) => {
7✔
574
        // this is the status gibberish `openssl dhparam` spews as it works
×
575
    })
7✔
576

7✔
577
    o.on('close', (code) => {
7✔
578
        if (code !== 0) {
×
579
            return done(`Error code: ${code}`)
×
580
        }
×
581

×
582
        log.info(`Saved to ${fpResolved}`)
×
583
        const content = this.config.get(filePath, 'binary')
×
584

×
585
        certsByHost.set('*.dhparam', content)
×
586
        done(null, certsByHost['*'].dhparam)
×
587
    })
7✔
588
}
7✔
589

14✔
590
exports.addOCSP = (server) => {
14✔
591
    if (!ocsp) {
9✔
592
        log.debug(`addOCSP: 'ocsp' not available`)
9✔
593
        return
9✔
594
    }
9✔
595

×
596
    if (server.listenerCount('OCSPRequest') > 0) {
×
597
        log.debug('OCSPRequest already listening')
×
598
        return
×
599
    }
×
600

×
601
    log.debug('adding OCSPRequest listener')
×
602
    server.on('OCSPRequest', (cert, issuer, ocr_cb) => {
×
603
        log.debug(`OCSPRequest: ${cert}`)
×
604
        ocsp.getOCSPURI(cert, async (err, uri) => {
×
605
            log.debug(`OCSP Request, URI: ${uri}, err=${err}`)
×
606
            if (err) return ocr_cb(err)
×
607
            if (uri === null) return ocr_cb() // not working OCSP server
×
608

×
609
            const req = ocsp.request.generate(cert, issuer)
×
610
            const cached = await ocspCache.probe(req.id)
×
611

×
612
            if (cached) {
×
613
                log.debug(`OCSP cache: ${util.inspect(cached)}`)
×
614
                return ocr_cb(null, cached.response)
×
615
            }
×
616

×
617
            const options = {
×
618
                url: uri,
×
619
                ocsp: req.data,
×
620
            }
×
621

×
622
            log.debug(`OCSP req:${util.inspect(req)}`)
×
623
            ocspCache.request(req.id, options, ocr_cb)
×
624
        })
×
625
    })
×
626
}
9✔
627

14✔
628
exports.shutdown = () => {
14✔
629
    if (ocsp) cleanOcspCache()
×
630
}
×
631

14✔
632
function cleanOcspCache() {
×
633
    log.debug(`Cleaning ocspCache. How many keys? ${Object.keys(ocspCache.cache).length}`)
×
634
    for (const key of Object.keys(ocspCache.cache)) {
×
635
        clearTimeout(ocspCache.cache[key].timer)
×
636
    }
×
637
}
×
638

14✔
639
exports.certsByHost = certsByHost
14✔
640
exports.ocsp = ocsp
14✔
641

14✔
642
exports.get_rejectUnauthorized = (rejectUnauthorized, port, port_list) => {
14✔
643
    // console.log(`rejectUnauthorized: ${rejectUnauthorized}, port ${port}, list: ${port_list}`)
8✔
644

8✔
645
    if (rejectUnauthorized) return true
8✔
646

7✔
647
    return !!port_list.includes(port)
7✔
648
}
8✔
649

14✔
650
function createServer(cb) {
6✔
651
    const server = net.createServer((cryptoSocket) => {
6✔
652
        const socket = new pluggableStream(cryptoSocket)
5✔
653

5✔
654
        exports.addOCSP(server)
5✔
655

5✔
656
        socket.upgrade = (cb2) => {
5✔
657
            log.debug('Upgrading to TLS')
1✔
658

1✔
659
            socket.clean()
1✔
660

1✔
661
            cryptoSocket.removeAllListeners('data')
1✔
662

1✔
663
            const options = { ...certsByHost['*'] }
1✔
664
            options.server = server // TLSSocket needs server for SNI to work
1✔
665

1✔
666
            options.rejectUnauthorized = exports.get_rejectUnauthorized(
1✔
667
                options.rejectUnauthorized,
1✔
668
                cryptoSocket.localPort,
1✔
669
                exports.cfg.main.requireAuthorized,
1✔
670
            )
1✔
671

1✔
672
            const cleartext = new tls.TLSSocket(cryptoSocket, options)
1✔
673

1✔
674
            pipe(cleartext, cryptoSocket)
1✔
675

1✔
676
            cleartext
1✔
677
                .on('error', (exception) => {
1✔
678
                    exception.source = 'tls'
1✔
679
                    socket.emit('error', exception)
1✔
680
                })
1✔
681
                .on('secure', () => {
1✔
682
                    log.debug('TLS secured.')
×
683
                    socket.emit('secure')
×
684
                    const cipher = cleartext.getCipher()
×
685
                    cipher.version = cleartext.getProtocol()
×
686
                    if (cb2)
×
687
                        cb2(cleartext.authorized, cleartext.authorizationError, cleartext.getPeerCertificate(), cipher)
×
688
                })
1✔
689

1✔
690
            socket.cleartext = cleartext
1✔
691

1✔
692
            if (socket._timeout) {
1✔
693
                cleartext.setTimeout(socket._timeout)
1✔
694
            }
1✔
695

1✔
696
            cleartext.setKeepAlive(socket._keepalive)
1✔
697

1✔
698
            socket.attach(socket.cleartext)
1✔
699
        }
1✔
700

5✔
701
        cb(socket)
5✔
702
    })
6✔
703

6✔
704
    return server
6✔
705
}
6✔
706

14✔
707
function getCertFor(host) {
1✔
708
    if (host && certsByHost[host]) return certsByHost[host]
1✔
709
    return certsByHost['*'] // the default TLS cert
×
710
}
1✔
711

14✔
712
function connect(conn_options = {}) {
7✔
713
    // called by outbound/client_pool, smtp_client, plugins/spamassassin,avg,clamd,
7✔
714
    // plugins/auth/auth_proxy
7✔
715

7✔
716
    const cryptoSocket = net.connect(conn_options)
7✔
717
    const socket = new pluggableStream(cryptoSocket)
7✔
718

7✔
719
    socket.upgrade = (options, cb2) => {
7✔
720
        socket.clean()
3✔
721
        cryptoSocket.removeAllListeners('data')
3✔
722

3✔
723
        if (exports.tls_valid) {
3✔
724
            const host = conn_options.host
1✔
725
            if (exports.cfg === undefined) exports.load_tls_ini()
1!
726
            if (exports.cfg.mutual_auth_hosts[host]) {
1✔
727
                options = { ...options, ...getCertFor(exports.cfg.mutual_auth_hosts[host]) }
1✔
728
            } else if (exports.cfg.mutual_auth_hosts_exclude[host]) {
1!
729
                // send no client cert
×
730
            } else if (exports.cfg.main.mutual_tls) {
×
731
                options = { ...options, ...getCertFor(host) }
×
732
            }
×
733
        }
1✔
734
        options.socket = cryptoSocket
3✔
735

3✔
736
        const cleartext = tls.connect(options)
3✔
737

3✔
738
        pipe(cleartext, cryptoSocket)
3✔
739

3✔
740
        cleartext.on('error', (err) => {
3✔
741
            err.source = 'tls'
2✔
742
            socket.emit('error', err)
2✔
743
        })
3✔
744

3✔
745
        cleartext.once('secureConnect', () => {
3✔
746
            log.debug('client TLS secured.')
2✔
747
            const cipher = cleartext.getCipher()
2✔
748
            cipher.version = cleartext.getProtocol()
2✔
749
            if (cb2) cb2(cleartext.authorized, cleartext.authorizationError, cleartext.getPeerCertificate(), cipher)
2✔
750
        })
3✔
751

3✔
752
        socket.cleartext = cleartext
3✔
753

3✔
754
        if (socket._timeout) {
3✔
755
            cleartext.setTimeout(socket._timeout)
1✔
756
        }
1✔
757

3✔
758
        cleartext.setKeepAlive(socket._keepalive)
3✔
759

3✔
760
        socket.attach(socket.cleartext)
3✔
761

3✔
762
        log.debug('client TLS upgrade in progress, awaiting secured.')
3✔
763
    }
3✔
764

7✔
765
    return socket
7✔
766
}
7✔
767

14✔
768
exports.connect = connect
14✔
769
exports.createConnection = connect
14✔
770
exports.Server = createServer
14✔
771
exports.createServer = createServer
14✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc