haraka 0.0.33 → 3.3.1
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/.githooks/pre-commit +41 -0
- package/.prettierignore +7 -0
- package/.qlty/.gitignore +7 -0
- package/.qlty/configs/.shellcheckrc +1 -0
- package/.qlty/qlty.toml +15 -0
- package/CHANGELOG.md +1898 -0
- package/CONTRIBUTORS.md +34 -0
- package/Dockerfile +50 -0
- package/LICENSE +22 -0
- package/Plugins.md +227 -0
- package/README.md +119 -4
- package/SECURITY.md +178 -0
- package/TODO +22 -0
- package/bin/haraka +593 -0
- package/bin/haraka_grep +32 -0
- package/config/aliases +2 -0
- package/config/auth_flat_file.ini +7 -0
- package/config/auth_vpopmaild.ini +9 -0
- package/config/connection.ini +79 -0
- package/config/delay_deny.ini +7 -0
- package/config/host_list +3 -0
- package/config/host_list_regex +6 -0
- package/config/http.ini +11 -0
- package/config/lmtp.ini +7 -0
- package/config/log.ini +11 -0
- package/config/outbound.bounce_message +18 -0
- package/config/outbound.bounce_message_html +36 -0
- package/config/outbound.bounce_message_image +106 -0
- package/config/outbound.ini +24 -0
- package/config/plugins +67 -0
- package/config/smtp.ini +37 -0
- package/config/smtp_bridge.ini +4 -0
- package/config/smtp_forward.ini +31 -0
- package/config/smtp_proxy.ini +27 -0
- package/config/tarpit.timeout +1 -0
- package/config/tls.ini +83 -0
- package/config/watch.ini +12 -0
- package/config/xclient.hosts +2 -0
- package/connection.js +1865 -0
- package/contrib/Haraka.cf +6 -0
- package/contrib/Haraka.pm +35 -0
- package/contrib/bad_smtp_server.pl +25 -0
- package/contrib/bsd-rc.d/haraka +63 -0
- package/contrib/debian-init.d/haraka +87 -0
- package/contrib/haraka.init +96 -0
- package/contrib/haraka.service +23 -0
- package/contrib/plugin2npm.sh +81 -0
- package/contrib/ubuntu-upstart/haraka.conf +27 -0
- package/docs/Body.md +1 -0
- package/docs/Config.md +1 -0
- package/docs/Connection.md +153 -0
- package/docs/CoreConfig.md +96 -0
- package/docs/CustomReturnCodes.md +3 -0
- package/docs/HAProxy.md +62 -0
- package/docs/Header.md +1 -0
- package/docs/Logging.md +129 -0
- package/docs/Outbound.md +210 -0
- package/docs/Plugins.md +372 -0
- package/docs/Results.md +7 -0
- package/docs/Transaction.md +135 -0
- package/docs/Tutorial.md +183 -0
- package/docs/deprecated/access.md +3 -0
- package/docs/deprecated/backscatterer.md +9 -0
- package/docs/deprecated/connect.rdns_access.md +53 -0
- package/docs/deprecated/data.headers.md +3 -0
- package/docs/deprecated/data.nomsgid.md +7 -0
- package/docs/deprecated/data.noreceived.md +11 -0
- package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
- package/docs/deprecated/dkim_sign.md +97 -0
- package/docs/deprecated/dkim_verify.md +28 -0
- package/docs/deprecated/dnsbl.md +80 -0
- package/docs/deprecated/dnswl.md +73 -0
- package/docs/deprecated/lookup_rdns.strict.md +67 -0
- package/docs/deprecated/mail_from.access.md +52 -0
- package/docs/deprecated/mail_from.blocklist.md +18 -0
- package/docs/deprecated/mail_from.nobounces.md +8 -0
- package/docs/deprecated/rcpt_to.access.md +53 -0
- package/docs/deprecated/rcpt_to.blocklist.md +18 -0
- package/docs/deprecated/rcpt_to.routes.md +3 -0
- package/docs/deprecated/rdns.regexp.md +30 -0
- package/docs/plugins/aliases.md +3 -0
- package/docs/plugins/auth/auth_bridge.md +34 -0
- package/docs/plugins/auth/auth_ldap.md +4 -0
- package/docs/plugins/auth/auth_proxy.md +36 -0
- package/docs/plugins/auth/auth_vpopmaild.md +33 -0
- package/docs/plugins/auth/flat_file.md +40 -0
- package/docs/plugins/block_me.md +18 -0
- package/docs/plugins/data.signatures.md +11 -0
- package/docs/plugins/delay_deny.md +23 -0
- package/docs/plugins/max_unrecognized_commands.md +6 -0
- package/docs/plugins/prevent_credential_leaks.md +22 -0
- package/docs/plugins/process_title.md +42 -0
- package/docs/plugins/queue/deliver.md +3 -0
- package/docs/plugins/queue/discard.md +32 -0
- package/docs/plugins/queue/lmtp.md +24 -0
- package/docs/plugins/queue/qmail-queue.md +16 -0
- package/docs/plugins/queue/quarantine.md +87 -0
- package/docs/plugins/queue/smtp_bridge.md +32 -0
- package/docs/plugins/queue/smtp_forward.md +127 -0
- package/docs/plugins/queue/smtp_proxy.md +68 -0
- package/docs/plugins/queue/test.md +7 -0
- package/docs/plugins/rcpt_to.in_host_list.md +34 -0
- package/docs/plugins/rcpt_to.max_count.md +3 -0
- package/docs/plugins/record_envelope_addresses.md +20 -0
- package/docs/plugins/relay.md +3 -0
- package/docs/plugins/reseed_rng.md +16 -0
- package/docs/plugins/status.md +41 -0
- package/docs/plugins/tarpit.md +50 -0
- package/docs/plugins/tls.md +235 -0
- package/docs/plugins/toobusy.md +27 -0
- package/docs/plugins/xclient.md +10 -0
- package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
- package/docs/tutorials/SettingUpOutbound.md +62 -0
- package/eslint.config.mjs +2 -0
- package/haraka.js +74 -0
- package/haraka.sh +2 -0
- package/http/html/404.html +58 -0
- package/http/html/index.html +47 -0
- package/http/package.json +21 -0
- package/line_socket.js +24 -0
- package/logger.js +322 -0
- package/outbound/client_pool.js +59 -0
- package/outbound/config.js +134 -0
- package/outbound/hmail.js +1504 -0
- package/outbound/index.js +349 -0
- package/outbound/qfile.js +93 -0
- package/outbound/queue.js +399 -0
- package/outbound/tls.js +85 -0
- package/outbound/todo.js +17 -0
- package/package.json +100 -4
- package/plugins/.eslintrc.yaml +3 -0
- package/plugins/auth/auth_base.js +261 -0
- package/plugins/auth/auth_bridge.js +20 -0
- package/plugins/auth/auth_proxy.js +227 -0
- package/plugins/auth/auth_vpopmaild.js +162 -0
- package/plugins/auth/flat_file.js +44 -0
- package/plugins/block_me.js +88 -0
- package/plugins/data.signatures.js +30 -0
- package/plugins/delay_deny.js +153 -0
- package/plugins/prevent_credential_leaks.js +61 -0
- package/plugins/process_title.js +197 -0
- package/plugins/profile.js +11 -0
- package/plugins/queue/deliver.js +12 -0
- package/plugins/queue/discard.js +27 -0
- package/plugins/queue/lmtp.js +45 -0
- package/plugins/queue/qmail-queue.js +93 -0
- package/plugins/queue/quarantine.js +133 -0
- package/plugins/queue/smtp_bridge.js +45 -0
- package/plugins/queue/smtp_forward.js +371 -0
- package/plugins/queue/smtp_proxy.js +142 -0
- package/plugins/queue/test.js +15 -0
- package/plugins/rcpt_to.host_list_base.js +65 -0
- package/plugins/rcpt_to.in_host_list.js +56 -0
- package/plugins/record_envelope_addresses.js +17 -0
- package/plugins/reseed_rng.js +7 -0
- package/plugins/status.js +274 -0
- package/plugins/tarpit.js +45 -0
- package/plugins/tls.js +164 -0
- package/plugins/toobusy.js +47 -0
- package/plugins/xclient.js +124 -0
- package/plugins.js +605 -0
- package/run_tests +11 -0
- package/server.js +827 -0
- package/smtp_client.js +504 -0
- package/test/.eslintrc.yaml +11 -0
- package/test/config/auth_flat_file.ini +5 -0
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/config/dhparams.pem +8 -0
- package/test/config/host_list +2 -0
- package/test/config/outbound_tls_cert.pem +1 -0
- package/test/config/outbound_tls_key.pem +1 -0
- package/test/config/plugins +7 -0
- package/test/config/smtp.ini +11 -0
- package/test/config/smtp_forward.ini +30 -0
- package/test/config/tls/example.com/_.example.com.key +28 -0
- package/test/config/tls/example.com/example.com.crt +25 -0
- package/test/config/tls/haraka.local.pem +51 -0
- package/test/config/tls.ini +45 -0
- package/test/config/tls_cert.pem +21 -0
- package/test/config/tls_key.pem +28 -0
- package/test/connection.js +820 -0
- package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
- package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
- package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
- package/test/fixtures/line_socket.js +21 -0
- package/test/fixtures/todo_qfile.txt +0 -0
- package/test/fixtures/util_hmailitem.js +156 -0
- package/test/installation/config/test-plugin-flat +1 -0
- package/test/installation/config/test-plugin.ini +10 -0
- package/test/installation/config/tls.ini +1 -0
- package/test/installation/node_modules/load_first/index.js +5 -0
- package/test/installation/node_modules/load_first/package.json +11 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
- package/test/installation/node_modules/test-plugin/package.json +5 -0
- package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
- package/test/installation/plugins/base_plugin.js +3 -0
- package/test/installation/plugins/folder_plugin/index.js +3 -0
- package/test/installation/plugins/folder_plugin/package.json +11 -0
- package/test/installation/plugins/inherits.js +7 -0
- package/test/installation/plugins/load_first.js +3 -0
- package/test/installation/plugins/plugin.js +1 -0
- package/test/installation/plugins/tls.js +3 -0
- package/test/logger.js +217 -0
- package/test/loud/config/dhparams.pem +0 -0
- package/test/loud/config/tls/goobered.pem +45 -0
- package/test/loud/config/tls.ini +43 -0
- package/test/mail_specimen/base64-root-part.txt +23 -0
- package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
- package/test/outbound/bounce_net_errors.js +133 -0
- package/test/outbound/bounce_rfc3464.js +226 -0
- package/test/outbound/hmail.js +210 -0
- package/test/outbound/index.js +385 -0
- package/test/outbound/qfile.js +124 -0
- package/test/outbound/queue.js +325 -0
- package/test/plugins/auth/auth_base.js +620 -0
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/auth_vpopmaild.js +81 -0
- package/test/plugins/auth/flat_file.js +123 -0
- package/test/plugins/block_me.js +141 -0
- package/test/plugins/data.signatures.js +111 -0
- package/test/plugins/delay_deny.js +262 -0
- package/test/plugins/prevent_credential_leaks.js +174 -0
- package/test/plugins/process_title.js +141 -0
- package/test/plugins/queue/deliver.js +98 -0
- package/test/plugins/queue/discard.js +78 -0
- package/test/plugins/queue/lmtp.js +137 -0
- package/test/plugins/queue/qmail-queue.js +98 -0
- package/test/plugins/queue/quarantine.js +80 -0
- package/test/plugins/queue/smtp_bridge.js +152 -0
- package/test/plugins/queue/smtp_forward.js +1023 -0
- package/test/plugins/queue/smtp_proxy.js +138 -0
- package/test/plugins/rcpt_to.host_list_base.js +102 -0
- package/test/plugins/rcpt_to.in_host_list.js +186 -0
- package/test/plugins/record_envelope_addresses.js +66 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/status.js +207 -0
- package/test/plugins/tarpit.js +90 -0
- package/test/plugins/tls.js +86 -0
- package/test/plugins/toobusy.js +198 -0
- package/test/plugins/xclient.js +119 -0
- package/test/plugins.js +230 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
- package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
- package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
- package/test/queue/zero-length +0 -0
- package/test/server.js +1012 -0
- package/test/smtp_client.js +1303 -0
- package/test/tls_socket.js +321 -0
- package/test/transaction.js +554 -0
- package/tls_socket.js +771 -0
- package/transaction.js +267 -0
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
const { makeConnection, makePlugin } = fixtures
|
|
8
|
+
const outbound = require('../../outbound')
|
|
9
|
+
const { TimerQueue } = require('haraka-utils')
|
|
10
|
+
|
|
11
|
+
const _set_up = () => {
|
|
12
|
+
this.plugin = makePlugin('status', { register: false })
|
|
13
|
+
this.plugin.outbound = outbound
|
|
14
|
+
|
|
15
|
+
this.connection = makeConnection()
|
|
16
|
+
this.connection.remote.is_local = true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('status', () => {
|
|
20
|
+
describe('register', () => {
|
|
21
|
+
beforeEach(_set_up)
|
|
22
|
+
|
|
23
|
+
it('loads the status plugin', () => {
|
|
24
|
+
assert.equal('status', this.plugin.name)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('access', () => {
|
|
29
|
+
beforeEach(_set_up)
|
|
30
|
+
|
|
31
|
+
it('remote', (t, done) => {
|
|
32
|
+
this.connection.remote.is_local = false
|
|
33
|
+
this.plugin.hook_unrecognized_command(
|
|
34
|
+
(code) => {
|
|
35
|
+
assert.equal(DENY, code)
|
|
36
|
+
done()
|
|
37
|
+
},
|
|
38
|
+
this.connection,
|
|
39
|
+
['STATUS', 'POOL LIST'],
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('pools', () => {
|
|
45
|
+
beforeEach(_set_up)
|
|
46
|
+
|
|
47
|
+
it('list_pools', (t, done) => {
|
|
48
|
+
this.connection.respond = (code, message) => {
|
|
49
|
+
const data = JSON.parse(message)
|
|
50
|
+
assert.equal('object', typeof data) // there should be one pools array for noncluster and more for cluster
|
|
51
|
+
done()
|
|
52
|
+
}
|
|
53
|
+
this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'POOL LIST'])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('queues', () => {
|
|
58
|
+
beforeEach(_set_up)
|
|
59
|
+
|
|
60
|
+
it('inspect_queue', (t, done) => {
|
|
61
|
+
// should list delivery_queue and temp_fail_queue per cluster children
|
|
62
|
+
outbound.temp_fail_queue = new TimerQueue(10)
|
|
63
|
+
outbound.temp_fail_queue.add('file1', 100, () => {})
|
|
64
|
+
outbound.temp_fail_queue.add('file2', 100, () => {})
|
|
65
|
+
|
|
66
|
+
this.connection.respond = (code, message) => {
|
|
67
|
+
const data = JSON.parse(message)
|
|
68
|
+
assert.equal(0, data.delivery_queue.length)
|
|
69
|
+
assert.equal(2, data.temp_fail_queue.length)
|
|
70
|
+
done()
|
|
71
|
+
}
|
|
72
|
+
this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE INSPECT'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('stat_queue', (t, done) => {
|
|
76
|
+
// should list files only
|
|
77
|
+
this.connection.respond = (code, message) => {
|
|
78
|
+
const data = JSON.parse(message)
|
|
79
|
+
assert.ok(/^\d+\/\d+\/\d+$/.test(data))
|
|
80
|
+
done()
|
|
81
|
+
}
|
|
82
|
+
this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE STATS'])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('list_queue', (t, done) => {
|
|
86
|
+
// should list files only
|
|
87
|
+
this.connection.respond = (code, message) => {
|
|
88
|
+
const data = JSON.parse(message)
|
|
89
|
+
assert.equal(0, data.length)
|
|
90
|
+
done()
|
|
91
|
+
}
|
|
92
|
+
this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE LIST'])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('discard_from_queue', (t, done) => {
|
|
96
|
+
const self = this
|
|
97
|
+
|
|
98
|
+
outbound.temp_fail_queue = new TimerQueue(10)
|
|
99
|
+
outbound.temp_fail_queue.add('file1', 10, () => {
|
|
100
|
+
assert.ok(false, 'This callback should not be called')
|
|
101
|
+
done()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
outbound.temp_fail_queue.add('file2', 2000, () => {})
|
|
105
|
+
|
|
106
|
+
this.plugin.hook_unrecognized_command(
|
|
107
|
+
() => {
|
|
108
|
+
self.connection.respond = (code, message) => {
|
|
109
|
+
const data = JSON.parse(message)
|
|
110
|
+
assert.equal(1, data.temp_fail_queue.length)
|
|
111
|
+
done()
|
|
112
|
+
}
|
|
113
|
+
self.plugin.hook_unrecognized_command(() => {}, self.connection, ['STATUS', 'QUEUE INSPECT'])
|
|
114
|
+
},
|
|
115
|
+
this.connection,
|
|
116
|
+
['STATUS', 'QUEUE DISCARD file1'],
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('push_email_at_queue', (t, done) => {
|
|
121
|
+
const timeout = setTimeout(() => {
|
|
122
|
+
assert.ok(false, 'Timeout')
|
|
123
|
+
done()
|
|
124
|
+
}, 1000)
|
|
125
|
+
|
|
126
|
+
outbound.temp_fail_queue.add('file', 1500, () => {
|
|
127
|
+
clearTimeout(timeout)
|
|
128
|
+
|
|
129
|
+
assert.ok(true)
|
|
130
|
+
done()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE PUSH file'])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('merge_worker_responses', () => {
|
|
138
|
+
beforeEach(_set_up)
|
|
139
|
+
|
|
140
|
+
it('POOL LIST merges objects from all workers', () => {
|
|
141
|
+
const result = JSON.parse(
|
|
142
|
+
JSON.stringify(
|
|
143
|
+
this.plugin.merge_worker_responses('POOL LIST', [
|
|
144
|
+
{ 'host1:25': { inUse: 1, size: 3 } },
|
|
145
|
+
{ 'host2:25': { inUse: 0, size: 2 } },
|
|
146
|
+
{},
|
|
147
|
+
]),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
assert.deepEqual(result, {
|
|
151
|
+
'host1:25': { inUse: 1, size: 3 },
|
|
152
|
+
'host2:25': { inUse: 0, size: 2 },
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('POOL LIST with all empty workers returns empty object', () => {
|
|
157
|
+
const result = JSON.parse(JSON.stringify(this.plugin.merge_worker_responses('POOL LIST', [{}, {}, {}])))
|
|
158
|
+
assert.deepEqual(result, {})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('QUEUE INSPECT merges queues from all workers', () => {
|
|
162
|
+
const result = JSON.parse(
|
|
163
|
+
JSON.stringify(
|
|
164
|
+
this.plugin.merge_worker_responses('QUEUE INSPECT', [
|
|
165
|
+
{ delivery_queue: [{ id: 'a' }], temp_fail_queue: [{ id: 'x', fire_time: 1 }] },
|
|
166
|
+
{ delivery_queue: [{ id: 'b' }], temp_fail_queue: [] },
|
|
167
|
+
{ delivery_queue: [], temp_fail_queue: [{ id: 'y', fire_time: 2 }] },
|
|
168
|
+
]),
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
assert.deepEqual(result, {
|
|
172
|
+
delivery_queue: [{ id: 'a' }, { id: 'b' }],
|
|
173
|
+
temp_fail_queue: [
|
|
174
|
+
{ id: 'x', fire_time: 1 },
|
|
175
|
+
{ id: 'y', fire_time: 2 },
|
|
176
|
+
],
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('QUEUE INSPECT with all empty queues returns empty lists', () => {
|
|
181
|
+
const result = JSON.parse(
|
|
182
|
+
JSON.stringify(
|
|
183
|
+
this.plugin.merge_worker_responses('QUEUE INSPECT', [
|
|
184
|
+
{ delivery_queue: [], temp_fail_queue: [] },
|
|
185
|
+
{ delivery_queue: [], temp_fail_queue: [] },
|
|
186
|
+
]),
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
assert.deepEqual(result, { delivery_queue: [], temp_fail_queue: [] })
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('QUEUE STATS sums across workers', () => {
|
|
193
|
+
const result = this.plugin.merge_worker_responses('QUEUE STATS', ['1/2/3', '0/1/0', '2/0/1'])
|
|
194
|
+
assert.equal(result, '3/3/4')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('QUEUE STATS with all zeros', () => {
|
|
198
|
+
const result = this.plugin.merge_worker_responses('QUEUE STATS', ['0/0/0', '0/0/0', '0/0/0'])
|
|
199
|
+
assert.equal(result, '0/0/0')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('unknown command returns results array unchanged', () => {
|
|
203
|
+
const result = this.plugin.merge_worker_responses('POOL UNKNOWN', [{ foo: 1 }, { foo: 2 }])
|
|
204
|
+
assert.equal(result.length, 2)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const { makeConnection, makePlugin } = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
describe('tarpit', () => {
|
|
9
|
+
let plugin
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
plugin = makePlugin('tarpit', { register: false })
|
|
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 = makeConnection({ withTxn: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('calls next immediately when no transaction', (t, done) => {
|
|
47
|
+
conn.transaction = null
|
|
48
|
+
plugin.tarpit((rc) => {
|
|
49
|
+
assert.equal(rc, undefined)
|
|
50
|
+
done()
|
|
51
|
+
}, conn)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('calls next immediately when no tarpit delay set', (t, done) => {
|
|
55
|
+
// No tarpit note on connection or transaction
|
|
56
|
+
plugin.tarpit((rc) => {
|
|
57
|
+
assert.equal(rc, undefined)
|
|
58
|
+
done()
|
|
59
|
+
}, conn)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('calls next immediately when connection.notes.tarpit is 0', (t, done) => {
|
|
63
|
+
conn.notes.tarpit = 0
|
|
64
|
+
plugin.tarpit((rc) => {
|
|
65
|
+
assert.equal(rc, undefined)
|
|
66
|
+
done()
|
|
67
|
+
}, conn)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('delays and calls next when connection.notes.tarpit is set', { timeout: 3000 }, (t, done) => {
|
|
71
|
+
conn.notes.tarpit = 0.1
|
|
72
|
+
const start = Date.now()
|
|
73
|
+
plugin.tarpit((rc) => {
|
|
74
|
+
assert.equal(rc, undefined)
|
|
75
|
+
assert.ok(Date.now() - start >= 90, 'should have waited ~100ms')
|
|
76
|
+
done()
|
|
77
|
+
}, conn)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('uses transaction.notes.tarpit when connection note is absent', { timeout: 3000 }, (t, done) => {
|
|
81
|
+
conn.transaction.notes.tarpit = 0.1
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
plugin.tarpit((rc) => {
|
|
84
|
+
assert.equal(rc, undefined)
|
|
85
|
+
assert.ok(Date.now() - start >= 90)
|
|
86
|
+
done()
|
|
87
|
+
}, conn)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
6
|
+
|
|
7
|
+
const { makeConnection, makePlugin } = require('haraka-test-fixtures')
|
|
8
|
+
|
|
9
|
+
const _set_up = () => {
|
|
10
|
+
this.plugin = makePlugin('tls', { register: false, configDir: 'test' })
|
|
11
|
+
this.connection = makeConnection()
|
|
12
|
+
|
|
13
|
+
// use test/config instead of ./config
|
|
14
|
+
this.plugin.net_utils.config = this.plugin.net_utils.config.module_config(path.resolve('test'))
|
|
15
|
+
|
|
16
|
+
this.plugin.tls_opts = {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('tls', () => {
|
|
20
|
+
beforeEach(_set_up)
|
|
21
|
+
|
|
22
|
+
const methods = ['register', 'upgrade_connection', 'advertise_starttls', 'emit_upgrade_msg']
|
|
23
|
+
for (const method of methods) {
|
|
24
|
+
it(`has function ${method}`, () => {
|
|
25
|
+
assert.equal(typeof this.plugin[method], 'function')
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('register', () => {
|
|
30
|
+
it('with certs, should register hooks', () => {
|
|
31
|
+
this.plugin.register()
|
|
32
|
+
assert.ok(Object.keys(this.plugin.hooks).length)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('emit_upgrade_msg', () => {
|
|
37
|
+
it('should emit a log message', () => {
|
|
38
|
+
assert.equal(
|
|
39
|
+
this.plugin.emit_upgrade_msg(this.connection, true, '', {
|
|
40
|
+
subject: {
|
|
41
|
+
CN: 'TLS.subject',
|
|
42
|
+
O: 'TLS.org',
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
'secured: verified=true cn="TLS.subject" organization="TLS.org"',
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should emit a log message with error', () => {
|
|
50
|
+
assert.equal(
|
|
51
|
+
this.plugin.emit_upgrade_msg(this.connection, true, 'oops', {
|
|
52
|
+
subject: {
|
|
53
|
+
CN: 'TLS.subject',
|
|
54
|
+
O: 'TLS.org',
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
'secured: verified=true error="oops" cn="TLS.subject" organization="TLS.org"',
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('upgrade_connection (STARTTLS injection)', () => {
|
|
63
|
+
// RFC 3207 §4: data pipelined after STARTTLS but before the TLS
|
|
64
|
+
// handshake must be discarded, not processed on the cleartext channel.
|
|
65
|
+
it('discards pipelined plaintext before the TLS handshake', () => {
|
|
66
|
+
const c = this.connection
|
|
67
|
+
c.tls = { advertised: true }
|
|
68
|
+
c.notes = {}
|
|
69
|
+
// attacker pipelined an injected command after STARTTLS
|
|
70
|
+
c.current_data = Buffer.from('RCPT TO:<victim@example.com>\r\n')
|
|
71
|
+
let dataAtUpgrade = 'UPGRADE_NOT_CALLED'
|
|
72
|
+
c.client = {
|
|
73
|
+
upgrade() {
|
|
74
|
+
dataAtUpgrade = c.current_data
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
c.respond = () => {} // bypass the real _process_data path
|
|
78
|
+
this.plugin.timeout = 0
|
|
79
|
+
|
|
80
|
+
this.plugin.upgrade_connection(() => {}, c, ['STARTTLS'])
|
|
81
|
+
|
|
82
|
+
assert.equal(dataAtUpgrade, null, 'buffer cleared before upgrade()')
|
|
83
|
+
assert.equal(c.current_data, null)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { beforeEach, describe, it } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const { makeConnection, makePlugin } = require('haraka-test-fixtures')
|
|
7
|
+
require('haraka-constants').import(global)
|
|
8
|
+
|
|
9
|
+
describe('toobusy', () => {
|
|
10
|
+
let plugin
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
plugin = makePlugin('toobusy', { register: false })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('register', () => {
|
|
17
|
+
it('registers connect hook with correct priority', () => {
|
|
18
|
+
const hooks = []
|
|
19
|
+
plugin.register_hook = function (hook, name, priority) {
|
|
20
|
+
hooks.push({ hook, name, priority })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
plugin.register()
|
|
24
|
+
|
|
25
|
+
assert.equal(hooks.length, 1, 'should register one hook')
|
|
26
|
+
assert.equal(hooks[0].hook, 'connect')
|
|
27
|
+
assert.equal(hooks[0].name, 'check_busy')
|
|
28
|
+
assert.equal(hooks[0].priority, -100)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('loads config on register', () => {
|
|
32
|
+
let loadConfigCalled = false
|
|
33
|
+
const originalLoadConfig = plugin.loadConfig
|
|
34
|
+
plugin.loadConfig = function () {
|
|
35
|
+
loadConfigCalled = true
|
|
36
|
+
return originalLoadConfig.call(this)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
plugin.register()
|
|
40
|
+
|
|
41
|
+
assert.equal(loadConfigCalled, true, 'loadConfig should be called')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('handles missing toobusy-js gracefully', () => {
|
|
45
|
+
assert.doesNotThrow(() => plugin.register())
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('loadConfig', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
plugin.register()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('gets toobusy.maxlag config value', () => {
|
|
55
|
+
let configArgs = []
|
|
56
|
+
|
|
57
|
+
plugin.config.get = function (key, type, callback) {
|
|
58
|
+
configArgs = [key, type]
|
|
59
|
+
return '70'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
plugin.loadConfig()
|
|
63
|
+
|
|
64
|
+
assert.equal(configArgs[0], 'toobusy.maxlag')
|
|
65
|
+
assert.equal(configArgs[1], 'value')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('passes callback to config.get for hot reload', () => {
|
|
69
|
+
let callbackProvided = false
|
|
70
|
+
|
|
71
|
+
plugin.config.get = function (key, type, callback) {
|
|
72
|
+
callbackProvided = typeof callback === 'function'
|
|
73
|
+
return '70'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
plugin.loadConfig()
|
|
77
|
+
|
|
78
|
+
assert.equal(callbackProvided, true, 'callback should be provided for hot reload')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('handles zero maxLag value', () => {
|
|
82
|
+
plugin.config.get = () => '0'
|
|
83
|
+
|
|
84
|
+
assert.doesNotThrow(() => {
|
|
85
|
+
plugin.loadConfig()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('handles non-numeric maxLag value', () => {
|
|
90
|
+
plugin.config.get = () => 'notanumber'
|
|
91
|
+
|
|
92
|
+
assert.doesNotThrow(() => {
|
|
93
|
+
plugin.loadConfig()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('handles empty string maxLag value', () => {
|
|
98
|
+
plugin.config.get = () => ''
|
|
99
|
+
|
|
100
|
+
assert.doesNotThrow(() => {
|
|
101
|
+
plugin.loadConfig()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('parses numeric maxLag as integer', () => {
|
|
106
|
+
plugin.config.get = () => '100'
|
|
107
|
+
|
|
108
|
+
assert.doesNotThrow(() => {
|
|
109
|
+
plugin.loadConfig()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('supports reload via callback', () => {
|
|
114
|
+
let callbackFn = null
|
|
115
|
+
|
|
116
|
+
plugin.config.get = function (key, type, callback) {
|
|
117
|
+
callbackFn = callback
|
|
118
|
+
return '70'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
plugin.loadConfig()
|
|
122
|
+
|
|
123
|
+
assert.equal(typeof callbackFn, 'function', 'callback should be provided')
|
|
124
|
+
assert.doesNotThrow(() => {
|
|
125
|
+
if (callbackFn) callbackFn()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('check_busy', () => {
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
plugin.register()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('calls next without args when not busy', (t, done) => {
|
|
136
|
+
plugin.config.get = () => '70'
|
|
137
|
+
plugin.loadConfig()
|
|
138
|
+
|
|
139
|
+
plugin.check_busy(function (...args) {
|
|
140
|
+
assert.equal(args.length, 0, 'should call next with no arguments')
|
|
141
|
+
done()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('initializes was_busy state', (t, done) => {
|
|
146
|
+
plugin.config.get = () => '70'
|
|
147
|
+
plugin.loadConfig()
|
|
148
|
+
|
|
149
|
+
plugin.check_busy(function () {
|
|
150
|
+
done()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('is a callable function', () => {
|
|
155
|
+
assert.equal(typeof plugin.check_busy, 'function')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('does not log when not busy', (t, done) => {
|
|
159
|
+
plugin.config.get = () => '70'
|
|
160
|
+
plugin.loadConfig()
|
|
161
|
+
|
|
162
|
+
let logCount = 0
|
|
163
|
+
plugin.logcrit = function () {
|
|
164
|
+
logCount++
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
plugin.check_busy(function () {
|
|
168
|
+
plugin.check_busy(function () {
|
|
169
|
+
assert.equal(logCount, 0, 'should not log when not busy')
|
|
170
|
+
done()
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('accepts next callback', (t, done) => {
|
|
176
|
+
plugin.config.get = () => '70'
|
|
177
|
+
plugin.loadConfig()
|
|
178
|
+
|
|
179
|
+
const nextFn = function () {
|
|
180
|
+
done()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
assert.doesNotThrow(() => {
|
|
184
|
+
plugin.check_busy(nextFn)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('works with connection context', (t, done) => {
|
|
189
|
+
plugin.config.get = () => '70'
|
|
190
|
+
plugin.loadConfig()
|
|
191
|
+
|
|
192
|
+
const conn = makeConnection()
|
|
193
|
+
plugin.check_busy.call(conn, function () {
|
|
194
|
+
done()
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const { makeConnection, makePlugin } = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
const _set_up = () => {
|
|
9
|
+
this.plugin = makePlugin('xclient', { register: false })
|
|
10
|
+
this.connection = makeConnection()
|
|
11
|
+
this.connection.capabilities = []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('xclient', () => {
|
|
15
|
+
beforeEach(_set_up)
|
|
16
|
+
|
|
17
|
+
describe('hook_capabilities', () => {
|
|
18
|
+
const cases = [
|
|
19
|
+
{ desc: 'adds XCLIENT for loopback IPv4 (127.0.0.1)', ip: '127.0.0.1', expected: true },
|
|
20
|
+
{ desc: 'adds XCLIENT for loopback IPv6 (::1)', ip: '::1', expected: true },
|
|
21
|
+
{ desc: 'does not add XCLIENT for non-loopback IP', ip: '10.0.0.1', expected: false },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
for (const { desc, ip, expected } of cases) {
|
|
25
|
+
it(desc, async () => {
|
|
26
|
+
this.connection.remote.ip = ip
|
|
27
|
+
await new Promise((resolve) => this.plugin.hook_capabilities(resolve, this.connection))
|
|
28
|
+
const hasXclient = this.connection.capabilities.some((c) => c.startsWith('XCLIENT'))
|
|
29
|
+
assert.equal(hasXclient, expected)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('hook_unrecognized_command', () => {
|
|
35
|
+
const callHook = (params) =>
|
|
36
|
+
new Promise((resolve) => {
|
|
37
|
+
this.plugin.hook_unrecognized_command((code) => resolve(code), this.connection, params)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const cases = [
|
|
41
|
+
{
|
|
42
|
+
desc: 'ignores non-XCLIENT commands',
|
|
43
|
+
params: ['EHLO', 'example.com'],
|
|
44
|
+
check: (code) => assert.equal(code, undefined),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
desc: 'denies XCLIENT when transaction is in progress',
|
|
48
|
+
setup: () => {
|
|
49
|
+
this.connection = makeConnection({ withTxn: true })
|
|
50
|
+
this.connection.capabilities = []
|
|
51
|
+
},
|
|
52
|
+
params: ['XCLIENT', 'ADDR=127.0.0.1'],
|
|
53
|
+
check: (code) => assert.equal(code, DENY),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
desc: 'denies XCLIENT from disallowed IP',
|
|
57
|
+
setup: () => {
|
|
58
|
+
this.connection.remote.ip = '10.0.0.1'
|
|
59
|
+
},
|
|
60
|
+
params: ['XCLIENT', 'ADDR=127.0.0.2'],
|
|
61
|
+
check: (code) => assert.equal(code, DENY),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
desc: 'denies XCLIENT with no valid IP address',
|
|
65
|
+
setup: () => {
|
|
66
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
67
|
+
},
|
|
68
|
+
params: ['XCLIENT', 'NAME=example.com'],
|
|
69
|
+
check: (code) => assert.equal(code, DENY),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
desc: 'accepts XCLIENT with valid IPv4 ADDR from allowed host',
|
|
73
|
+
setup: () => {
|
|
74
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
75
|
+
},
|
|
76
|
+
params: ['XCLIENT', 'ADDR=1.2.3.4'],
|
|
77
|
+
check: (code) => assert.ok(code === NEXT_HOOK || code === undefined),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
desc: 'accepts XCLIENT with valid IPv6 ADDR from allowed host',
|
|
81
|
+
setup: () => {
|
|
82
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
83
|
+
},
|
|
84
|
+
params: ['XCLIENT', 'ADDR=IPV6:2001:db8::1'],
|
|
85
|
+
check: (code) => assert.ok(code === NEXT_HOOK || code === undefined),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
desc: 'accepts XCLIENT with ADDR and NAME, skipping rdns lookup',
|
|
89
|
+
setup: () => {
|
|
90
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
91
|
+
},
|
|
92
|
+
params: ['XCLIENT', 'ADDR=1.2.3.4 NAME=example.com'],
|
|
93
|
+
check: (code) => assert.equal(code, NEXT_HOOK),
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for (const { desc, setup, params, check } of cases) {
|
|
98
|
+
it(desc, async () => {
|
|
99
|
+
if (setup) setup()
|
|
100
|
+
const code = await callHook(params)
|
|
101
|
+
check(code)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('DESTPORT type', () => {
|
|
107
|
+
it('stores local.port as an integer (587/465 auth check)', async () => {
|
|
108
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
109
|
+
await new Promise((resolve) => {
|
|
110
|
+
this.plugin.hook_unrecognized_command(() => resolve(), this.connection, [
|
|
111
|
+
'XCLIENT',
|
|
112
|
+
'ADDR=1.2.3.4 DESTPORT=587',
|
|
113
|
+
])
|
|
114
|
+
})
|
|
115
|
+
assert.strictEqual(this.connection.local.port, 587)
|
|
116
|
+
assert.equal(typeof this.connection.local.port, 'number')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|