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

haraka / Haraka / 23886219068

02 Apr 2026 05:53AM UTC coverage: 68.803% (+5.2%) from 63.652%
23886219068

Pull #3546

github

web-flow
Merge af009ebb7 into a9c0fd522
Pull Request #3546: test: add tests for tls_socket & smtp_client

1414 of 1905 branches covered (74.23%)

21 of 26 new or added lines in 3 files covered. (80.77%)

2 existing lines in 1 file now uncovered.

6592 of 9581 relevant lines covered (68.8%)

23.77 hits per line

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

77.08
/tls_socket.js
1
'use strict'
11✔
2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

54✔
185
    return res
54✔
186
}
56✔
187

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

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

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

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

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

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

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

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

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

10✔
244
    this.cfg = cfg
10✔
245

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

10✔
251
    return cfg
10✔
252
}
10✔
253

11✔
254
exports.applySocketOpts = (name) => {
11✔
255
    // https://nodejs.org/api/tls.html#tls_new_tls_tlssocket_socket_options
45✔
256
    const TLSSocketOptions = [
45✔
257
        // 'server'        // manually added
45✔
258
        'isServer',
45✔
259
        'requestCert',
45✔
260
        'rejectUnauthorized',
45✔
261
        'NPNProtocols',
45✔
262
        'ALPNProtocols',
45✔
263
        'session',
45✔
264
        'requestOCSP',
45✔
265
        'secureContext',
45✔
266
        'SNICallback',
45✔
267
    ]
45✔
268

45✔
269
    // https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
45✔
270
    const createSecureContextOptions = [
45✔
271
        'key',
45✔
272
        'cert',
45✔
273
        'dhparam',
45✔
274
        'pfx',
45✔
275
        'passphrase',
45✔
276
        'ca',
45✔
277
        'crl',
45✔
278
        'ciphers',
45✔
279
        'minVersion',
45✔
280
        'honorCipherOrder',
45✔
281
        'ecdhCurve',
45✔
282
        'secureProtocol',
45✔
283
        'secureOptions',
45✔
284
        'sessionIdContext',
45✔
285
    ]
45✔
286

45✔
287
    for (const opt of [...TLSSocketOptions, ...createSecureContextOptions]) {
45✔
288
        if (this.cfg[name] && this.cfg[name][opt] !== undefined) {
1,035!
289
            // if the setting exists in tls.ini [name]
×
290
            certsByHost.set([name, opt], this.cfg[name][opt])
×
291
        } else if (this.cfg.main[opt] !== undefined) {
1,035✔
292
            // save settings in tls.ini [main] to each CN
400✔
293
            certsByHost.set([name, opt], this.cfg.main[opt])
400✔
294
        } else {
1,035✔
295
            // defaults
635✔
296
            switch (opt) {
635✔
297
                case 'sessionIdContext':
635✔
298
                    certsByHost.set([name, opt], 'haraka')
45✔
299
                    break
45✔
300
                case 'isServer':
635✔
301
                    certsByHost.set([name, opt], true)
45✔
302
                    break
45✔
303
                case 'key':
635✔
304
                    certsByHost.set([name, opt], 'tls_key.pem')
1✔
305
                    break
1✔
306
                case 'cert':
635✔
307
                    certsByHost.set([name, opt], 'tls_cert.pem')
1✔
308
                    break
1✔
309
                case 'dhparam':
635✔
310
                    certsByHost.set([name, opt], 'dhparams.pem')
1✔
311
                    break
1✔
312
                case 'SNICallback':
635✔
313
                    certsByHost.set([name, opt], exports.SNICallback)
45✔
314
                    break
45✔
315
            }
635✔
316
        }
635✔
317
    }
1,035✔
318
}
45✔
319

11✔
320
exports.load_default_opts = () => {
11✔
321
    const cfg = certsByHost['*']
9✔
322

9✔
323
    if (cfg.dhparam && typeof cfg.dhparam === 'string') {
9✔
324
        log.debug(`loading dhparams from ${cfg.dhparam}`)
9✔
325
        certsByHost.set('*.dhparam', this.config.get(cfg.dhparam, 'binary'))
9✔
326
    }
9✔
327

9✔
328
    if (cfg.ca && typeof cfg.ca === 'string') {
9!
329
        log.info(`loading CA certs from ${cfg.ca}`)
×
330
        certsByHost.set('*.ca', this.config.get(cfg.ca, 'binary'))
×
331
    }
×
332

9✔
333
    // make non-array key/cert option into Arrays with one entry
9✔
334
    if (!Array.isArray(cfg.key)) cfg.key = [cfg.key]
9✔
335
    if (!Array.isArray(cfg.cert)) cfg.cert = [cfg.cert]
9✔
336

9✔
337
    if (cfg.key.length !== cfg.cert.length) {
9!
338
        log.error(`number of keys (${cfg.key.length}) not equal to certs (${cfg.cert.length}).`)
×
339
    }
×
340

9✔
341
    // if key file has already been loaded, it'll be a Buffer.
9✔
342
    if (typeof cfg.key[0] === 'string') {
9✔
343
        // turn key/cert file names into actual key/cert binary data
9✔
344
        const asArray = cfg.key.map((keyFileName) => {
9✔
345
            if (!keyFileName) return
9!
346
            const key = this.config.get(keyFileName, 'binary')
9✔
347
            if (!key) {
9✔
348
                log.error(`tls key ${path.join(this.config.root_path, keyFileName)} could not be loaded.`)
1✔
349
            }
1✔
350
            return key
9✔
351
        })
9✔
352
        certsByHost.set('*.key', asArray)
9✔
353
    }
9✔
354

9✔
355
    if (typeof cfg.cert[0] === 'string') {
9✔
356
        const asArray = cfg.cert.map((certFileName) => {
9✔
357
            if (!certFileName) return
9!
358
            const cert = this.config.get(certFileName, 'binary')
9✔
359
            if (!cert) {
9✔
360
                log.error(`tls cert ${path.join(this.config.root_path, certFileName)} could not be loaded.`)
1✔
361
            }
1✔
362
            return cert
9✔
363
        })
9✔
364
        certsByHost.set('*.cert', asArray)
9✔
365
    }
9✔
366

9✔
367
    if (cfg.cert[0] && cfg.key[0]) {
9✔
368
        this.tls_valid = true
8✔
369

8✔
370
        // now that all opts are applied, generate TLS context
8✔
371
        this.ensureDhparams(() => {
8✔
372
            ctxByHost['*'] = tls.createSecureContext(cfg)
8✔
373
        })
8✔
374
    }
8✔
375
}
9✔
376

11✔
377
exports.SNICallback = function (servername, sniDone) {
11✔
378
    log.debug(`SNI servername: ${servername}`)
×
379

×
380
    sniDone(null, ctxByHost[servername] || ctxByHost['*'])
×
381
}
×
382

11✔
383
exports.get_certs_dir = async (tlsDir) => {
11✔
384
    const r = {}
18✔
385
    const watcher = async () => {
18✔
386
        exports.get_certs_dir(tlsDir)
×
387
    }
×
388
    const dirOpts = { type: 'binary', watchCb: watcher }
18✔
389

18✔
390
    const files = await this.config.getDir(tlsDir, dirOpts)
18✔
391
    for (const file of files) {
18✔
392
        try {
54✔
393
            r[file.path] = await exports.parse_x509(file.data.toString())
54✔
394
        } catch (err) {
54!
395
            log.debug(err.message)
×
396
        }
×
397
    }
54✔
398

18✔
399
    log.debug(`found ${Object.keys(r).length} files in config/tls`)
18✔
400
    if (Object.keys(r).length === 0) return
18!
401

18✔
402
    const s = {} // certs by name (CN)
18✔
403

18✔
404
    for (const fp in r) {
18✔
405
        if (r[fp].expire && r[fp].expire < new Date()) {
54!
406
            log.error(`${fp} expired on ${r[fp].expire}`)
×
407
        }
×
408

54✔
409
        // a file with a key and no cert, get name from file
54✔
410
        if (!r[fp].names) r[fp].names = [path.parse(fp).name]
54✔
411

54✔
412
        for (let name of r[fp].names) {
54✔
413
            if (name[0] === '_') name = name.replace('_', '*') // windows
54✔
414
            if (s[name] === undefined) s[name] = {}
54✔
415
            if (!s[name].key && r[fp].keys) s[name].key = r[fp].keys[0]
54✔
416
            if (!s[name].cert && r[fp].chain) {
54✔
417
                s[name].cert = r[fp].chain[0]
36✔
418
                s[name].file = fp
36✔
419
            }
36✔
420
        }
54✔
421
    }
54✔
422

18✔
423
    for (const cn in s) {
18✔
424
        if (!s[cn].cert || !s[cn].key) {
36!
425
            delete s[cn]
×
426
            continue
×
427
        }
×
428

36✔
429
        this.applySocketOpts(cn) // from tls.ini
36✔
430
        certsByHost.set([cn, 'cert'], Buffer.from(s[cn].cert))
36✔
431
        certsByHost.set([cn, 'key'], Buffer.from(s[cn].key))
36✔
432
        certsByHost.set([cn, 'dhparam'], certsByHost['*'].dhparam, true)
36✔
433

36✔
434
        // all opts are applied, generate TLS context
36✔
435
        try {
36✔
436
            ctxByHost[cn] = tls.createSecureContext(certsByHost.get([cn]))
36✔
437
        } catch (err) {
36!
438
            log.error(`CN '${cn}' loading got: ${err.message}`)
×
439
            delete ctxByHost[cn]
×
440
            delete certsByHost[cn]
×
441
        }
×
442
    }
36✔
443

18✔
444
    log.info(`found ${Object.keys(s).length} TLS certs in config/tls`)
18✔
445

18✔
446
    return certsByHost // used only by tests
18✔
447
}
18✔
448

11✔
449
function openssl(crt, ...params) {
36✔
450
    return new Promise((resolve) => {
36✔
451
        let crtTxt = ''
36✔
452
        let errTxt = ''
36✔
453

36✔
454
        const o = spawn('openssl', params, { timeout: 2000 })
36✔
455
        o.stdout.on('data', (data) => {
36✔
456
            crtTxt += data
36✔
457
        })
36✔
458

36✔
459
        o.stderr.on('data', (data) => {
36✔
460
            errTxt += data
36✔
461
        })
36✔
462

36✔
463
        o.on('close', (code) => {
36✔
464
            if (code !== 0) {
36!
NEW
465
                log.error(`openssl ${params.join(' ')} failed with code ${code}: ${errTxt.trim()}`)
×
466
            }
×
467
            resolve(crtTxt)
36✔
468
        })
36✔
469

36✔
470
        o.stdin.write(crt)
36✔
471
        o.stdin.write('\n')
36✔
472
    })
36✔
473
}
36✔
474

11✔
475
exports.getSocketOpts = async (name) => {
11✔
476
    // startup time, load the config/tls dir
18✔
477
    if (!certsByHost['*']) this.load_tls_ini()
18✔
478

18✔
479
    try {
18✔
480
        await this.get_certs_dir('tls')
18✔
481
    } catch (err) {
18!
482
        if (err.code !== 'ENOENT') {
×
NEW
483
            log.error(err.message)
×
484
        }
×
485
    }
×
486

18✔
487
    return certsByHost[name] || certsByHost['*']
18!
488
}
18✔
489

11✔
490
function pipe(cleartext, socket) {
5✔
491
    cleartext.socket = socket
5✔
492

5✔
493
    function onError(e) {}
5✔
494

5✔
495
    function onClose() {
5✔
496
        socket.removeListener('error', onError)
5✔
497
        socket.removeListener('close', onClose)
5✔
498
    }
5✔
499

5✔
500
    socket.on('error', onError)
5✔
501
    socket.on('close', onClose)
5✔
502
}
5✔
503

11✔
504
exports.ensureDhparams = (done) => {
11✔
505
    // empty/missing dhparams file
8✔
506
    if (certsByHost['*'].dhparam) {
8✔
507
        return done(null, certsByHost['*'].dhparam)
8✔
508
    }
8✔
509

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

×
512
    const filePath = this.cfg.main.dhparam || 'dhparams.pem'
×
513
    const fpResolved = path.resolve(exports.config.root_path, filePath)
8✔
514

8✔
515
    log.info(`Generating a 2048 bit dhparams file at ${fpResolved}`)
8✔
516

8✔
517
    const o = spawn('openssl', ['dhparam', '-out', fpResolved, '2048'], { timeout: 30000 })
8✔
518
    o.stdout.on('data', (data) => {
8✔
519
        // normally empty output
×
520
        log.debug(data)
×
521
    })
8✔
522

8✔
523
    o.stderr.on('data', (data) => {
8✔
524
        // this is the status gibberish `openssl dhparam` spews as it works
×
525
    })
8✔
526

8✔
527
    o.on('close', (code) => {
8✔
528
        if (code !== 0) {
×
529
            return done(`Error code: ${code}`)
×
530
        }
×
531

×
532
        log.info(`Saved to ${fpResolved}`)
×
533
        const content = this.config.get(filePath, 'binary')
×
534

×
535
        certsByHost.set('*.dhparam', content)
×
536
        done(null, certsByHost['*'].dhparam)
×
537
    })
8✔
538
}
8✔
539

11✔
540
exports.addOCSP = (server) => {
11✔
541
    if (!ocsp) {
7✔
542
        log.debug(`addOCSP: 'ocsp' not available`)
7✔
543
        return
7✔
544
    }
7✔
545

×
546
    if (server.listenerCount('OCSPRequest') > 0) {
×
547
        log.debug('OCSPRequest already listening')
×
548
        return
×
549
    }
×
550

×
551
    log.debug('adding OCSPRequest listener')
×
552
    server.on('OCSPRequest', (cert, issuer, ocr_cb) => {
×
553
        log.debug(`OCSPRequest: ${cert}`)
×
554
        ocsp.getOCSPURI(cert, async (err, uri) => {
×
555
            log.debug(`OCSP Request, URI: ${uri}, err=${err}`)
×
556
            if (err) return ocr_cb(err)
×
557
            if (uri === null) return ocr_cb() // not working OCSP server
×
558

×
559
            const req = ocsp.request.generate(cert, issuer)
×
560
            const cached = await ocspCache.probe(req.id)
×
561

×
562
            if (cached) {
×
563
                log.debug(`OCSP cache: ${util.inspect(cached)}`)
×
564
                return ocr_cb(null, cached.response)
×
565
            }
×
566

×
567
            const options = {
×
568
                url: uri,
×
569
                ocsp: req.data,
×
570
            }
×
571

×
572
            log.debug(`OCSP req:${util.inspect(req)}`)
×
573
            ocspCache.request(req.id, options, ocr_cb)
×
574
        })
×
575
    })
×
576
}
7✔
577

11✔
578
exports.shutdown = () => {
11✔
579
    if (ocsp) cleanOcspCache()
×
580
}
×
581

11✔
582
function cleanOcspCache() {
×
583
    log.debug(`Cleaning ocspCache. How many keys? ${Object.keys(ocspCache.cache).length}`)
×
NEW
584
    for (const key of Object.keys(ocspCache.cache)) {
×
585
        clearTimeout(ocspCache.cache[key].timer)
×
NEW
586
    }
×
587
}
×
588

11✔
589
exports.certsByHost = certsByHost
11✔
590
exports.ocsp = ocsp
11✔
591

11✔
592
exports.get_rejectUnauthorized = (rejectUnauthorized, port, port_list) => {
11✔
593
    // console.log(`rejectUnauthorized: ${rejectUnauthorized}, port ${port}, list: ${port_list}`)
10✔
594

10✔
595
    if (rejectUnauthorized) return true
10✔
596

9✔
597
    return !!port_list.includes(port)
9✔
598
}
10✔
599

11✔
600
function createServer(cb) {
7✔
601
    const server = net.createServer((cryptoSocket) => {
7✔
602
        const socket = new pluggableStream(cryptoSocket)
5✔
603

5✔
604
        exports.addOCSP(server)
5✔
605

5✔
606
        socket.upgrade = (cb2) => {
5✔
607
            log.debug('Upgrading to TLS')
5✔
608

5✔
609
            socket.clean()
5✔
610

5✔
611
            cryptoSocket.removeAllListeners('data')
5✔
612

5✔
613
            const options = { ...certsByHost['*'] }
5✔
614
            options.server = server // TLSSocket needs server for SNI to work
5✔
615

5✔
616
            options.rejectUnauthorized = exports.get_rejectUnauthorized(
5✔
617
                options.rejectUnauthorized,
5✔
618
                cryptoSocket.localPort,
5✔
619
                exports.cfg.main.requireAuthorized,
5✔
620
            )
5✔
621

5✔
622
            const cleartext = new tls.TLSSocket(cryptoSocket, options)
5✔
623

5✔
624
            pipe(cleartext, cryptoSocket)
5✔
625

5✔
626
            cleartext
5✔
627
                .on('error', (exception) => {
5✔
628
                    exception.source = 'tls'
1✔
629
                    socket.emit('error', exception)
1✔
630
                })
5✔
631
                .on('secure', () => {
5✔
632
                    log.debug('TLS secured.')
4✔
633
                    socket.emit('secure')
4✔
634
                    const cipher = cleartext.getCipher()
4✔
635
                    cipher.version = cleartext.getProtocol()
4✔
636
                    if (cb2)
4✔
637
                        cb2(cleartext.authorized, cleartext.authorizationError, cleartext.getPeerCertificate(), cipher)
4✔
638
                })
5✔
639

5✔
640
            socket.cleartext = cleartext
5✔
641

5✔
642
            if (socket._timeout) {
5✔
643
                cleartext.setTimeout(socket._timeout)
5✔
644
            }
5✔
645

5✔
646
            cleartext.setKeepAlive(socket._keepalive)
5✔
647

5✔
648
            socket.attach(socket.cleartext)
5✔
649
        }
5✔
650

5✔
651
        cb(socket)
5✔
652
    })
7✔
653

7✔
654
    return server
7✔
655
}
7✔
656

11✔
657
function getCertFor(host) {
×
658
    if (host && certsByHost[host]) return certsByHost[host]
×
659
    return certsByHost['*'] // the default TLS cert
×
660
}
×
661

11✔
662
function connect(conn_options = {}) {
1✔
663
    // called by outbound/client_pool, smtp_client, plugins/spamassassin,avg,clamd,
1✔
664
    // plugins/auth/auth_proxy
1✔
665

1✔
666
    const cryptoSocket = net.connect(conn_options)
1✔
667
    const socket = new pluggableStream(cryptoSocket)
1✔
668

1✔
669
    socket.upgrade = (options, cb2) => {
1✔
670
        socket.clean()
×
671
        cryptoSocket.removeAllListeners('data')
×
672

×
673
        if (exports.tls_valid) {
×
674
            const host = conn_options.host
×
675
            if (exports.cfg === undefined) exports.load_tls_ini()
×
676
            if (exports.cfg.mutual_auth_hosts[host]) {
×
677
                options = { ...options, ...getCertFor(exports.cfg.mutual_auth_hosts[host]) }
×
678
            } else if (exports.cfg.mutual_auth_hosts_exclude[host]) {
×
679
                // send no client cert
×
680
            } else if (exports.cfg.main.mutual_tls) {
×
681
                options = { ...options, ...getCertFor(host) }
×
682
            }
×
683
        }
×
684
        options.socket = cryptoSocket
×
685

×
NEW
686
        const cleartext = tls.connect(options)
×
687

×
688
        pipe(cleartext, cryptoSocket)
×
689

×
690
        cleartext.on('error', (err) => {
×
691
            if (err.reason) log.error(`client TLS error: ${err}`)
×
692
        })
×
693

×
694
        cleartext.once('secureConnect', () => {
×
695
            log.debug('client TLS secured.')
×
696
            const cipher = cleartext.getCipher()
×
697
            cipher.version = cleartext.getProtocol()
×
698
            if (cb2) cb2(cleartext.authorized, cleartext.authorizationError, cleartext.getPeerCertificate(), cipher)
×
699
        })
×
700

×
701
        socket.cleartext = cleartext
×
702

×
703
        if (socket._timeout) {
×
704
            cleartext.setTimeout(socket._timeout)
×
705
        }
×
706

×
707
        cleartext.setKeepAlive(socket._keepalive)
×
708

×
709
        socket.attach(socket.cleartext)
×
710

×
711
        log.debug('client TLS upgrade in progress, awaiting secured.')
×
712
    }
×
713

1✔
714
    return socket
1✔
715
}
1✔
716

11✔
717
exports.connect = connect
11✔
718
exports.createConnection = connect
11✔
719
exports.Server = createServer
11✔
720
exports.createServer = createServer
11✔
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