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

haraka / Haraka / 26464322293

26 May 2026 05:29PM UTC coverage: 73.576% (-0.06%) from 73.64%
26464322293

Pull #3577

github

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

1824 of 2354 branches covered (77.49%)

185 of 195 new or added lines in 2 files covered. (94.87%)

90 existing lines in 6 files now uncovered.

8111 of 11024 relevant lines covered (73.58%)

26.41 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 = {}
104✔
160
    if (!string) return res
104✔
161

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

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

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

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

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

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

102✔
185
    return res
102✔
186
}
104✔
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
77✔
306
    const TLSSocketOptions = [
77✔
307
        // 'server'        // manually added
77✔
308
        'isServer',
77✔
309
        'requestCert',
77✔
310
        'rejectUnauthorized',
77✔
311
        'NPNProtocols',
77✔
312
        'ALPNProtocols',
77✔
313
        'session',
77✔
314
        'requestOCSP',
77✔
315
        'secureContext',
77✔
316
        'SNICallback',
77✔
317
    ]
77✔
318

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

77✔
337
    for (const opt of [...TLSSocketOptions, ...createSecureContextOptions]) {
77✔
338
        if (this.cfg[name] && this.cfg[name][opt] !== undefined) {
1,771!
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,771✔
342
            // save settings in tls.ini [main] to each CN
683✔
343
            certsByHost.set([name, opt], this.cfg.main[opt])
683✔
344
        } else {
1,771✔
345
            // defaults
1,088✔
346
            switch (opt) {
1,088✔
347
                case 'sessionIdContext':
1,088✔
348
                    certsByHost.set([name, opt], 'haraka')
77✔
349
                    break
77✔
350
                case 'isServer':
1,088✔
351
                    certsByHost.set([name, opt], true)
77✔
352
                    break
77✔
353
                case 'key':
1,088✔
354
                    certsByHost.set([name, opt], 'tls_key.pem')
2✔
355
                    break
2✔
356
                case 'cert':
1,088✔
357
                    certsByHost.set([name, opt], 'tls_cert.pem')
2✔
358
                    break
2✔
359
                case 'dhparam':
1,088✔
360
                    certsByHost.set([name, opt], 'dhparams.pem')
2✔
361
                    break
2✔
362
                case 'SNICallback':
1,088✔
363
                    certsByHost.set([name, opt], exports.SNICallback)
77✔
364
                    break
77✔
365
            }
1,088✔
366
        }
1,088✔
367
    }
1,771✔
368
}
77✔
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}`)
6✔
429

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

35✔
537
    return certsByHost[name] || certsByHost['*']
35!
538
}
35✔
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) {
16✔
592
        log.debug(`addOCSP: 'ocsp' not available`)
16✔
593
        return
16✔
594
    }
16✔
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
}
16✔
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}`)
15✔
644

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

14✔
647
    return !!port_list.includes(port)
14✔
648
}
15✔
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