Haraka 3.1.6 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/CONTRIBUTORS.md +8 -8
- package/Plugins.md +99 -99
- package/config/smtp_forward.ini +10 -0
- package/config/smtp_proxy.ini +10 -0
- package/connection.js +25 -8
- package/docs/plugins/queue/smtp_forward.md +19 -3
- package/docs/plugins/queue/smtp_proxy.md +10 -2
- package/haraka.js +1 -1
- package/outbound/hmail.js +39 -39
- package/outbound/index.js +4 -4
- package/outbound/tls.js +2 -43
- package/package.json +49 -48
- package/plugins/auth/auth_base.js +9 -3
- package/plugins/auth/auth_proxy.js +14 -11
- package/plugins/block_me.js +4 -2
- package/plugins/prevent_credential_leaks.js +3 -1
- package/plugins/process_title.js +6 -6
- package/plugins/queue/qmail-queue.js +15 -19
- package/plugins/queue/smtp_forward.js +12 -4
- package/plugins/queue/smtp_proxy.js +14 -3
- package/plugins/tls.js +13 -5
- package/plugins/xclient.js +3 -1
- package/server.js +5 -3
- package/smtp_client.js +20 -11
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/connection.js +24 -0
- package/test/outbound/bounce_net_errors.js +3 -2
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/flat_file.js +128 -0
- package/test/plugins/block_me.js +157 -0
- package/test/plugins/data.signatures.js +114 -0
- package/test/plugins/delay_deny.js +263 -0
- package/test/plugins/prevent_credential_leaks.js +178 -0
- package/test/plugins/process_title.js +135 -0
- package/test/plugins/queue/deliver.js +99 -0
- package/test/plugins/queue/discard.js +79 -0
- package/test/plugins/queue/lmtp.js +138 -0
- package/test/plugins/queue/qmail-queue.js +99 -0
- package/test/plugins/queue/quarantine.js +81 -0
- package/test/plugins/queue/smtp_bridge.js +154 -0
- package/test/plugins/queue/smtp_forward.js +42 -6
- package/test/plugins/queue/smtp_proxy.js +139 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/tarpit.js +91 -0
- package/test/plugins/tls.js +25 -0
- package/test/plugins/toobusy.js +21 -0
- package/test/plugins/xclient.js +14 -0
- package/test/server.js +59 -0
- package/test/smtp_client.js +45 -12
- package/test/tls_socket.js +82 -0
- package/tls_socket.js +50 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach, before } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
before(() => {
|
|
9
|
+
require('haraka-constants').import(global)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('queue/qmail-queue', () => {
|
|
13
|
+
describe('register', () => {
|
|
14
|
+
it('throws when qmail-queue binary is not found', () => {
|
|
15
|
+
const plugin = new fixtures.plugin('queue/qmail-queue')
|
|
16
|
+
assert.throws(() => plugin.register(), /Cannot find qmail-queue binary/)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('load_qmail_queue_ini', () => {
|
|
21
|
+
it('loads config with enable_outbound boolean', () => {
|
|
22
|
+
const plugin = new fixtures.plugin('queue/qmail-queue')
|
|
23
|
+
plugin.load_qmail_queue_ini()
|
|
24
|
+
assert.ok(typeof plugin.cfg.main.enable_outbound === 'boolean')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('hook_queue', () => {
|
|
29
|
+
let plugin, conn
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
plugin = new fixtures.plugin('queue/qmail-queue')
|
|
33
|
+
plugin.load_qmail_queue_ini()
|
|
34
|
+
plugin.queue_exec = '/bin/echo' // use a real binary that exists
|
|
35
|
+
conn = fixtures.connection.createConnection()
|
|
36
|
+
conn.init_transaction()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('calls next() when there is no transaction', (t, done) => {
|
|
40
|
+
const connNoTxn = fixtures.connection.createConnection()
|
|
41
|
+
plugin.hook_queue((rc) => {
|
|
42
|
+
assert.equal(rc, undefined)
|
|
43
|
+
done()
|
|
44
|
+
}, connNoTxn)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('calls next() when queue.wants is set to another plugin', (t, done) => {
|
|
48
|
+
conn.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
49
|
+
plugin.hook_queue((rc) => {
|
|
50
|
+
assert.equal(rc, undefined)
|
|
51
|
+
done()
|
|
52
|
+
}, conn)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('calls next() when queue.wants is set to discard', (t, done) => {
|
|
56
|
+
conn.transaction.notes.set('queue.wants', 'discard')
|
|
57
|
+
plugin.hook_queue((rc) => {
|
|
58
|
+
assert.equal(rc, undefined)
|
|
59
|
+
done()
|
|
60
|
+
}, conn)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('build_envelope', () => {
|
|
65
|
+
let plugin
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
plugin = new fixtures.plugin('queue/qmail-queue')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('formats F<sender>\\0(T<rcpt>\\0)*\\0 with no padding', () => {
|
|
71
|
+
const buf = plugin.build_envelope({
|
|
72
|
+
mail_from: { address: 'snd@example.com' },
|
|
73
|
+
rcpt_to: [{ address: 'a@example.com' }, { address: 'b@example.com' }],
|
|
74
|
+
})
|
|
75
|
+
assert.deepEqual(buf, Buffer.from('Fsnd@example.com\x00Ta@example.com\x00Tb@example.com\x00\x00', 'utf8'))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('is not truncated for large recipient sets', () => {
|
|
79
|
+
const rcpt_to = []
|
|
80
|
+
for (let i = 0; i < 500; i++) {
|
|
81
|
+
rcpt_to.push({ address: `recipient-with-a-fairly-long-localpart-${i}@example.com` })
|
|
82
|
+
}
|
|
83
|
+
const buf = plugin.build_envelope({ mail_from: { address: 'snd@example.com' }, rcpt_to })
|
|
84
|
+
assert.equal((buf.toString('utf8').match(/T/g) || []).length, 500)
|
|
85
|
+
assert.ok(buf.length > 4096, 'exceeds the old fixed 4096-byte cap')
|
|
86
|
+
assert.ok(buf.toString('utf8').includes('recipient-with-a-fairly-long-localpart-499@example.com'))
|
|
87
|
+
assert.equal(buf[buf.length - 1], 0)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('encodes non-ASCII (SMTPUTF8) addresses correctly', () => {
|
|
91
|
+
const buf = plugin.build_envelope({
|
|
92
|
+
mail_from: { address: 'sénder@exämple.com' },
|
|
93
|
+
rcpt_to: [{ address: 'reçìpient@exämple.com' }],
|
|
94
|
+
})
|
|
95
|
+
assert.ok(buf.includes(Buffer.from('sénder@exämple.com', 'utf8')))
|
|
96
|
+
assert.ok(buf.includes(Buffer.from('reçìpient@exämple.com', 'utf8')))
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
describe('queue/quarantine', () => {
|
|
9
|
+
let plugin
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
plugin = new fixtures.plugin('queue/quarantine')
|
|
13
|
+
plugin.load_quarantine_ini()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('zeroPad', () => {
|
|
17
|
+
it('pads a single digit number to 2 digits', () => {
|
|
18
|
+
assert.equal(plugin.zeroPad(5, 2), '05')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('pads a single digit to 4 digits', () => {
|
|
22
|
+
assert.equal(plugin.zeroPad(7, 4), '0007')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('does not pad when already at target length', () => {
|
|
26
|
+
assert.equal(plugin.zeroPad(12, 2), '12')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not pad when number exceeds target length', () => {
|
|
30
|
+
assert.equal(plugin.zeroPad(2025, 2), '2025')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('handles 0 correctly', () => {
|
|
34
|
+
assert.equal(plugin.zeroPad(0, 2), '00')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('get_base_dir', () => {
|
|
39
|
+
it('returns default quarantine path when not configured', () => {
|
|
40
|
+
plugin.cfg = { main: {} }
|
|
41
|
+
assert.equal(plugin.get_base_dir(), '/var/spool/haraka/quarantine')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns configured quarantine_path when set', () => {
|
|
45
|
+
plugin.cfg = { main: { quarantine_path: '/tmp/my-quarantine' } }
|
|
46
|
+
assert.equal(plugin.get_base_dir(), '/tmp/my-quarantine')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('quarantine hook', () => {
|
|
51
|
+
let conn
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
conn = fixtures.connection.createConnection()
|
|
55
|
+
conn.init_transaction()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('calls next() when no quarantine conditions are met', (t, done) => {
|
|
59
|
+
plugin.quarantine((rc) => {
|
|
60
|
+
assert.equal(rc, undefined)
|
|
61
|
+
done()
|
|
62
|
+
}, conn)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('calls next() when connection.notes.quarantine is falsy', (t, done) => {
|
|
66
|
+
conn.notes.quarantine = false
|
|
67
|
+
plugin.quarantine((rc) => {
|
|
68
|
+
assert.equal(rc, undefined)
|
|
69
|
+
done()
|
|
70
|
+
}, conn)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('calls next() when queue.wants is set to something other than quarantine', (t, done) => {
|
|
74
|
+
conn.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
75
|
+
plugin.quarantine((rc) => {
|
|
76
|
+
assert.equal(rc, undefined)
|
|
77
|
+
done()
|
|
78
|
+
}, conn)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach, before } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
before(() => {
|
|
9
|
+
require('haraka-constants').import(global)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('queue/smtp_bridge', () => {
|
|
13
|
+
describe('hook_data_post', () => {
|
|
14
|
+
let plugin, conn
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
plugin = new fixtures.plugin('queue/smtp_bridge')
|
|
18
|
+
plugin.load_flat_ini()
|
|
19
|
+
conn = fixtures.connection.createConnection()
|
|
20
|
+
conn.init_transaction()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('calls next() when no transaction', (t, done) => {
|
|
24
|
+
const connNoTxn = fixtures.connection.createConnection()
|
|
25
|
+
plugin.hook_data_post((rc) => {
|
|
26
|
+
assert.equal(rc, undefined)
|
|
27
|
+
done()
|
|
28
|
+
}, connNoTxn)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('copies auth_user from connection notes to transaction notes', (t, done) => {
|
|
32
|
+
conn.notes.auth_user = 'alice'
|
|
33
|
+
conn.notes.auth_passwd = 'secret'
|
|
34
|
+
plugin.hook_data_post((rc) => {
|
|
35
|
+
assert.equal(rc, undefined)
|
|
36
|
+
assert.equal(conn.transaction.notes.auth_user, 'alice')
|
|
37
|
+
done()
|
|
38
|
+
}, conn)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('copies auth_passwd from connection notes to transaction notes', (t, done) => {
|
|
42
|
+
conn.notes.auth_user = 'bob'
|
|
43
|
+
conn.notes.auth_passwd = 'mypassword'
|
|
44
|
+
plugin.hook_data_post(() => {
|
|
45
|
+
assert.equal(conn.transaction.notes.auth_passwd, 'mypassword')
|
|
46
|
+
done()
|
|
47
|
+
}, conn)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('copies undefined auth values when not set on connection', (t, done) => {
|
|
51
|
+
plugin.hook_data_post(() => {
|
|
52
|
+
assert.equal(conn.transaction.notes.auth_user, undefined)
|
|
53
|
+
assert.equal(conn.transaction.notes.auth_passwd, undefined)
|
|
54
|
+
done()
|
|
55
|
+
}, conn)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('hook_get_mx', () => {
|
|
60
|
+
let plugin
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
plugin = new fixtures.plugin('queue/smtp_bridge')
|
|
64
|
+
plugin.load_flat_ini()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('returns OK with default priority 10 and configured host', (t, done) => {
|
|
68
|
+
plugin.cfg.main = { host: 'relay.example.com' }
|
|
69
|
+
const hmail = { todo: { notes: {} } }
|
|
70
|
+
plugin.hook_get_mx(
|
|
71
|
+
(rc, mx) => {
|
|
72
|
+
assert.equal(rc, OK)
|
|
73
|
+
assert.equal(mx.priority, 10)
|
|
74
|
+
assert.equal(mx.exchange, 'relay.example.com')
|
|
75
|
+
done()
|
|
76
|
+
},
|
|
77
|
+
hmail,
|
|
78
|
+
'example.com',
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('uses configured priority when set', (t, done) => {
|
|
83
|
+
plugin.cfg.main = { host: 'relay.example.com', priority: 20 }
|
|
84
|
+
const hmail = { todo: { notes: {} } }
|
|
85
|
+
plugin.hook_get_mx(
|
|
86
|
+
(rc, mx) => {
|
|
87
|
+
assert.equal(rc, OK)
|
|
88
|
+
assert.equal(mx.priority, 20)
|
|
89
|
+
done()
|
|
90
|
+
},
|
|
91
|
+
hmail,
|
|
92
|
+
'example.com',
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('passes auth_type from config', (t, done) => {
|
|
97
|
+
plugin.cfg.main = { host: 'relay.example.com', auth_type: 'PLAIN' }
|
|
98
|
+
const hmail = { todo: { notes: {} } }
|
|
99
|
+
plugin.hook_get_mx(
|
|
100
|
+
(rc, mx) => {
|
|
101
|
+
assert.equal(rc, OK)
|
|
102
|
+
assert.equal(mx.auth_type, 'PLAIN')
|
|
103
|
+
done()
|
|
104
|
+
},
|
|
105
|
+
hmail,
|
|
106
|
+
'example.com',
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('passes port from config', (t, done) => {
|
|
111
|
+
plugin.cfg.main = { host: 'relay.example.com', port: '587' }
|
|
112
|
+
const hmail = { todo: { notes: {} } }
|
|
113
|
+
plugin.hook_get_mx(
|
|
114
|
+
(rc, mx) => {
|
|
115
|
+
assert.equal(rc, OK)
|
|
116
|
+
assert.equal(mx.port, '587')
|
|
117
|
+
done()
|
|
118
|
+
},
|
|
119
|
+
hmail,
|
|
120
|
+
'example.com',
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('passes auth_user and auth_pass from hmail notes', (t, done) => {
|
|
125
|
+
plugin.cfg.main = { host: 'relay.example.com' }
|
|
126
|
+
const hmail = { todo: { notes: { auth_user: 'alice', auth_passwd: 'secret' } } }
|
|
127
|
+
plugin.hook_get_mx(
|
|
128
|
+
(rc, mx) => {
|
|
129
|
+
assert.equal(rc, OK)
|
|
130
|
+
assert.equal(mx.auth_user, 'alice')
|
|
131
|
+
assert.equal(mx.auth_pass, 'secret')
|
|
132
|
+
done()
|
|
133
|
+
},
|
|
134
|
+
hmail,
|
|
135
|
+
'example.com',
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('sets port null and auth_type null when not configured', (t, done) => {
|
|
140
|
+
plugin.cfg.main = { host: 'relay.example.com' }
|
|
141
|
+
const hmail = { todo: { notes: {} } }
|
|
142
|
+
plugin.hook_get_mx(
|
|
143
|
+
(rc, mx) => {
|
|
144
|
+
assert.equal(rc, OK)
|
|
145
|
+
assert.equal(mx.port, null)
|
|
146
|
+
assert.equal(mx.auth_type, null)
|
|
147
|
+
done()
|
|
148
|
+
},
|
|
149
|
+
hmail,
|
|
150
|
+
'example.com',
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -42,7 +42,7 @@ function makeHmail(notes = {}) {
|
|
|
42
42
|
class MockSMTPClient extends EventEmitter {
|
|
43
43
|
constructor() {
|
|
44
44
|
super()
|
|
45
|
-
this.
|
|
45
|
+
this.smtputf8 = false
|
|
46
46
|
this.response = ['250 OK']
|
|
47
47
|
this.next = null
|
|
48
48
|
this.commands = []
|
|
@@ -140,11 +140,47 @@ describe('smtp_forward register', () => {
|
|
|
140
140
|
assert.equal(plugin.hooks.queue, undefined)
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
it('
|
|
144
|
-
const plugin =
|
|
145
|
-
plugin.register
|
|
146
|
-
assert.
|
|
147
|
-
assert.ok(
|
|
143
|
+
it('populates tls_options after register (no-op shape)', () => {
|
|
144
|
+
const plugin = makePlugin()
|
|
145
|
+
assert.ok(plugin.tls_options, 'tls_options should be populated after register')
|
|
146
|
+
assert.ok(Array.isArray(plugin.tls_options.no_tls_hosts))
|
|
147
|
+
assert.ok(Array.isArray(plugin.tls_options.force_tls_hosts))
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// ─── tls_options ─────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
describe('smtp_forward tls_options', () => {
|
|
154
|
+
const tls_socket = require('../../../tls_socket')
|
|
155
|
+
let origTlsConfig, origTlsCfg
|
|
156
|
+
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
// Redirect tls_socket.config at test/config so tls.ini fixtures load.
|
|
159
|
+
origTlsConfig = tls_socket.config
|
|
160
|
+
origTlsCfg = tls_socket.cfg
|
|
161
|
+
tls_socket.config = require('haraka-config').module_config(path.resolve('test'))
|
|
162
|
+
tls_socket.cfg = undefined
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
tls_socket.config = origTlsConfig
|
|
167
|
+
tls_socket.cfg = origTlsCfg
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('inherits rejectUnauthorized/minVersion/ciphers from tls.ini [main]', () => {
|
|
171
|
+
const plugin = makePlugin()
|
|
172
|
+
assert.equal(plugin.tls_options.rejectUnauthorized, false)
|
|
173
|
+
assert.equal(plugin.tls_options.minVersion, 'TLSv1')
|
|
174
|
+
assert.ok(plugin.tls_options.ciphers)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('reload re-derives tls_options', () => {
|
|
178
|
+
const plugin = makePlugin()
|
|
179
|
+
const first = plugin.tls_options
|
|
180
|
+
plugin.load_smtp_forward_ini()
|
|
181
|
+
assert.ok(plugin.tls_options)
|
|
182
|
+
assert.notEqual(plugin.tls_options, first, 'reload returns a fresh object')
|
|
183
|
+
assert.equal(plugin.tls_options.rejectUnauthorized, false)
|
|
148
184
|
})
|
|
149
185
|
})
|
|
150
186
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { describe, it, beforeEach, afterEach, before } = require('node:test')
|
|
6
|
+
|
|
7
|
+
const fixtures = require('haraka-test-fixtures')
|
|
8
|
+
|
|
9
|
+
const tls_socket = require('../../../tls_socket')
|
|
10
|
+
|
|
11
|
+
before(() => {
|
|
12
|
+
require('haraka-constants').import(global)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('queue/smtp_proxy', () => {
|
|
16
|
+
let plugin, conn
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
plugin = new fixtures.plugin('queue/smtp_proxy')
|
|
20
|
+
plugin.load_smtp_proxy_ini()
|
|
21
|
+
conn = fixtures.connection.createConnection()
|
|
22
|
+
conn.init_transaction()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('hook_rset', () => {
|
|
26
|
+
it('calls next() when no smtp_client in notes', (t, done) => {
|
|
27
|
+
plugin.hook_rset((rc) => {
|
|
28
|
+
assert.equal(rc, undefined)
|
|
29
|
+
done()
|
|
30
|
+
}, conn)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('releases smtp_client and calls next() when smtp_client exists', (t, done) => {
|
|
34
|
+
let released = false
|
|
35
|
+
conn.notes.smtp_client = {
|
|
36
|
+
release: () => {
|
|
37
|
+
released = true
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
plugin.hook_rset((rc) => {
|
|
41
|
+
assert.equal(released, true)
|
|
42
|
+
assert.equal(conn.notes.smtp_client, undefined)
|
|
43
|
+
done()
|
|
44
|
+
}, conn)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('hook_quit', () => {
|
|
49
|
+
it('calls next() when no smtp_client in notes', (t, done) => {
|
|
50
|
+
plugin.hook_quit((rc) => {
|
|
51
|
+
assert.equal(rc, undefined)
|
|
52
|
+
done()
|
|
53
|
+
}, conn)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('is the same function as hook_rset', () => {
|
|
57
|
+
assert.equal(plugin.hook_rset, plugin.hook_quit)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('hook_disconnect', () => {
|
|
62
|
+
it('calls next() when no smtp_client in notes', (t, done) => {
|
|
63
|
+
plugin.hook_disconnect((rc) => {
|
|
64
|
+
assert.equal(rc, undefined)
|
|
65
|
+
done()
|
|
66
|
+
}, conn)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('releases and calls next when smtp_client exists', (t, done) => {
|
|
70
|
+
let released = false
|
|
71
|
+
let callNextCalled = false
|
|
72
|
+
conn.notes.smtp_client = {
|
|
73
|
+
release: () => {
|
|
74
|
+
released = true
|
|
75
|
+
},
|
|
76
|
+
call_next: () => {
|
|
77
|
+
callNextCalled = true
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
plugin.hook_disconnect((rc) => {
|
|
81
|
+
assert.equal(released, true)
|
|
82
|
+
assert.equal(callNextCalled, true)
|
|
83
|
+
assert.equal(conn.notes.smtp_client, undefined)
|
|
84
|
+
done()
|
|
85
|
+
}, conn)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('hook_queue', () => {
|
|
90
|
+
it('calls next() when transaction is missing', (t, done) => {
|
|
91
|
+
const connNoTxn = fixtures.connection.createConnection()
|
|
92
|
+
plugin.hook_queue((rc) => {
|
|
93
|
+
assert.equal(rc, undefined)
|
|
94
|
+
done()
|
|
95
|
+
}, connNoTxn)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('calls next() when smtp_client is not in notes', (t, done) => {
|
|
99
|
+
plugin.hook_queue((rc) => {
|
|
100
|
+
assert.equal(rc, undefined)
|
|
101
|
+
done()
|
|
102
|
+
}, conn)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('tls_options', () => {
|
|
107
|
+
let origTlsConfig, origTlsCfg
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
origTlsConfig = tls_socket.config
|
|
111
|
+
origTlsCfg = tls_socket.cfg
|
|
112
|
+
tls_socket.config = require('haraka-config').module_config(path.resolve('test'))
|
|
113
|
+
tls_socket.cfg = undefined
|
|
114
|
+
// re-derive with test/config in scope
|
|
115
|
+
plugin.load_smtp_proxy_ini()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
tls_socket.config = origTlsConfig
|
|
120
|
+
tls_socket.cfg = origTlsCfg
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('populates tls_options from tls.ini [main]', () => {
|
|
124
|
+
assert.ok(plugin.tls_options)
|
|
125
|
+
assert.equal(plugin.tls_options.rejectUnauthorized, false)
|
|
126
|
+
assert.equal(plugin.tls_options.minVersion, 'TLSv1')
|
|
127
|
+
assert.ok(plugin.tls_options.ciphers)
|
|
128
|
+
assert.ok(Array.isArray(plugin.tls_options.no_tls_hosts))
|
|
129
|
+
assert.ok(Array.isArray(plugin.tls_options.force_tls_hosts))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('reload re-derives tls_options', () => {
|
|
133
|
+
const first = plugin.tls_options
|
|
134
|
+
plugin.load_smtp_proxy_ini()
|
|
135
|
+
assert.ok(plugin.tls_options)
|
|
136
|
+
assert.notEqual(plugin.tls_options, first)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { describe, it } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
describe('reseed_rng', () => {
|
|
9
|
+
describe('hook_init_child', () => {
|
|
10
|
+
it('calls Math.seedrandom with a hex string and calls next', (t, done) => {
|
|
11
|
+
const plugin = new fixtures.plugin('reseed_rng')
|
|
12
|
+
let called = false
|
|
13
|
+
let calledArg
|
|
14
|
+
Math.seedrandom = (arg) => {
|
|
15
|
+
called = true
|
|
16
|
+
calledArg = arg
|
|
17
|
+
}
|
|
18
|
+
plugin.hook_init_child((rc) => {
|
|
19
|
+
delete Math.seedrandom
|
|
20
|
+
assert.equal(rc, undefined)
|
|
21
|
+
assert.ok(called, 'Math.seedrandom should have been called')
|
|
22
|
+
assert.equal(typeof calledArg, 'string')
|
|
23
|
+
assert.ok(calledArg.length > 0)
|
|
24
|
+
done()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('throws when Math.seedrandom is not defined', () => {
|
|
29
|
+
const plugin = new fixtures.plugin('reseed_rng')
|
|
30
|
+
delete Math.seedrandom
|
|
31
|
+
assert.throws(() => plugin.hook_init_child(() => {}), /Math\.seedrandom is not a function/)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
describe('tarpit', () => {
|
|
9
|
+
let plugin
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
plugin = new fixtures.plugin('tarpit')
|
|
13
|
+
plugin.config.get = () => ({ main: {} })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('register', () => {
|
|
17
|
+
it('registers tarpit on all default hooks', () => {
|
|
18
|
+
const registered = []
|
|
19
|
+
plugin.register_hook = (hook) => registered.push(hook)
|
|
20
|
+
plugin.register()
|
|
21
|
+
assert.ok(registered.includes('connect'))
|
|
22
|
+
assert.ok(registered.includes('ehlo'))
|
|
23
|
+
assert.ok(registered.includes('mail'))
|
|
24
|
+
assert.ok(registered.includes('rcpt'))
|
|
25
|
+
assert.ok(registered.includes('data'))
|
|
26
|
+
assert.ok(registered.includes('queue'))
|
|
27
|
+
assert.ok(registered.includes('quit'))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('registers only configured hooks when hooks_to_delay is set', () => {
|
|
31
|
+
plugin.config.get = () => ({ main: { hooks_to_delay: 'ehlo, mail' } })
|
|
32
|
+
const registered = []
|
|
33
|
+
plugin.register_hook = (hook) => registered.push(hook)
|
|
34
|
+
plugin.register()
|
|
35
|
+
assert.deepEqual(registered, ['ehlo', 'mail'])
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('tarpit', () => {
|
|
40
|
+
let conn
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
conn = fixtures.connection.createConnection()
|
|
44
|
+
conn.init_transaction()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('calls next immediately when no transaction', (t, done) => {
|
|
48
|
+
conn.transaction = null
|
|
49
|
+
plugin.tarpit((rc) => {
|
|
50
|
+
assert.equal(rc, undefined)
|
|
51
|
+
done()
|
|
52
|
+
}, conn)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('calls next immediately when no tarpit delay set', (t, done) => {
|
|
56
|
+
// No tarpit note on connection or transaction
|
|
57
|
+
plugin.tarpit((rc) => {
|
|
58
|
+
assert.equal(rc, undefined)
|
|
59
|
+
done()
|
|
60
|
+
}, conn)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('calls next immediately when connection.notes.tarpit is 0', (t, done) => {
|
|
64
|
+
conn.notes.tarpit = 0
|
|
65
|
+
plugin.tarpit((rc) => {
|
|
66
|
+
assert.equal(rc, undefined)
|
|
67
|
+
done()
|
|
68
|
+
}, conn)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('delays and calls next when connection.notes.tarpit is set', { timeout: 3000 }, (t, done) => {
|
|
72
|
+
conn.notes.tarpit = 0.1
|
|
73
|
+
const start = Date.now()
|
|
74
|
+
plugin.tarpit((rc) => {
|
|
75
|
+
assert.equal(rc, undefined)
|
|
76
|
+
assert.ok(Date.now() - start >= 90, 'should have waited ~100ms')
|
|
77
|
+
done()
|
|
78
|
+
}, conn)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('uses transaction.notes.tarpit when connection note is absent', { timeout: 3000 }, (t, done) => {
|
|
82
|
+
conn.transaction.notes.tarpit = 0.1
|
|
83
|
+
const start = Date.now()
|
|
84
|
+
plugin.tarpit((rc) => {
|
|
85
|
+
assert.equal(rc, undefined)
|
|
86
|
+
assert.ok(Date.now() - start >= 90)
|
|
87
|
+
done()
|
|
88
|
+
}, conn)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
package/test/plugins/tls.js
CHANGED
|
@@ -60,4 +60,29 @@ describe('tls', () => {
|
|
|
60
60
|
)
|
|
61
61
|
})
|
|
62
62
|
})
|
|
63
|
+
|
|
64
|
+
describe('upgrade_connection (STARTTLS injection)', () => {
|
|
65
|
+
// RFC 3207 §4: data pipelined after STARTTLS but before the TLS
|
|
66
|
+
// handshake must be discarded, not processed on the cleartext channel.
|
|
67
|
+
it('discards pipelined plaintext before the TLS handshake', () => {
|
|
68
|
+
const c = this.connection
|
|
69
|
+
c.tls = { advertised: true }
|
|
70
|
+
c.notes = {}
|
|
71
|
+
// attacker pipelined an injected command after STARTTLS
|
|
72
|
+
c.current_data = Buffer.from('RCPT TO:<victim@example.com>\r\n')
|
|
73
|
+
let dataAtUpgrade = 'UPGRADE_NOT_CALLED'
|
|
74
|
+
c.client = {
|
|
75
|
+
upgrade() {
|
|
76
|
+
dataAtUpgrade = c.current_data
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
c.respond = () => {} // bypass the real _process_data path
|
|
80
|
+
this.plugin.timeout = 0
|
|
81
|
+
|
|
82
|
+
this.plugin.upgrade_connection(() => {}, c, ['STARTTLS'])
|
|
83
|
+
|
|
84
|
+
assert.equal(dataAtUpgrade, null, 'buffer cleared before upgrade()')
|
|
85
|
+
assert.equal(c.current_data, null)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
63
88
|
})
|