Haraka 3.2.1 → 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.
Files changed (82) hide show
  1. package/.githooks/pre-commit +41 -0
  2. package/.prettierignore +1 -0
  3. package/.qlty/.gitignore +7 -0
  4. package/.qlty/configs/.shellcheckrc +1 -0
  5. package/.qlty/qlty.toml +15 -0
  6. package/CHANGELOG.md +29 -5
  7. package/CONTRIBUTORS.md +5 -5
  8. package/README.md +6 -3
  9. package/bin/haraka +12 -4
  10. package/config/connection.ini +6 -0
  11. package/connection.js +67 -68
  12. package/contrib/bsd-rc.d/haraka +2 -0
  13. package/docs/CoreConfig.md +2 -0
  14. package/docs/HAProxy.md +4 -1
  15. package/eslint.config.mjs +2 -30
  16. package/haraka.js +2 -2
  17. package/line_socket.js +6 -33
  18. package/outbound/hmail.js +18 -29
  19. package/outbound/index.js +3 -3
  20. package/outbound/queue.js +8 -5
  21. package/package.json +49 -46
  22. package/plugins/auth/auth_proxy.js +7 -4
  23. package/plugins/block_me.js +1 -1
  24. package/plugins/delay_deny.js +1 -1
  25. package/plugins/queue/qmail-queue.js +1 -1
  26. package/plugins/queue/quarantine.js +5 -5
  27. package/plugins/queue/smtp_bridge.js +1 -1
  28. package/plugins/queue/smtp_proxy.js +2 -2
  29. package/plugins/status.js +2 -2
  30. package/plugins/toobusy.js +1 -1
  31. package/plugins.js +4 -3
  32. package/server.js +172 -28
  33. package/smtp_client.js +2 -1
  34. package/test/connection.js +119 -2
  35. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  36. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  37. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  38. package/test/fixtures/line_socket.js +1 -1
  39. package/test/fixtures/util_hmailitem.js +2 -3
  40. package/test/outbound/index.js +6 -7
  41. package/test/outbound/qfile.js +1 -1
  42. package/test/outbound/queue.js +2 -2
  43. package/test/plugins/auth/auth_base.js +17 -17
  44. package/test/plugins/auth/auth_bridge.js +3 -3
  45. package/test/plugins/auth/auth_vpopmaild.js +3 -3
  46. package/test/plugins/auth/flat_file.js +16 -21
  47. package/test/plugins/block_me.js +7 -23
  48. package/test/plugins/data.signatures.js +17 -20
  49. package/test/plugins/delay_deny.js +3 -4
  50. package/test/plugins/prevent_credential_leaks.js +17 -21
  51. package/test/plugins/process_title.js +12 -6
  52. package/test/plugins/queue/deliver.js +7 -8
  53. package/test/plugins/queue/discard.js +3 -4
  54. package/test/plugins/queue/lmtp.js +5 -6
  55. package/test/plugins/queue/qmail-queue.js +7 -8
  56. package/test/plugins/queue/quarantine.js +3 -4
  57. package/test/plugins/queue/smtp_bridge.js +5 -7
  58. package/test/plugins/queue/smtp_forward.js +49 -60
  59. package/test/plugins/queue/smtp_proxy.js +6 -7
  60. package/test/plugins/rcpt_to.host_list_base.js +6 -9
  61. package/test/plugins/rcpt_to.in_host_list.js +6 -11
  62. package/test/plugins/record_envelope_addresses.js +33 -60
  63. package/test/plugins/reseed_rng.js +3 -3
  64. package/test/plugins/status.js +4 -5
  65. package/test/plugins/tarpit.js +3 -4
  66. package/test/plugins/tls.js +3 -5
  67. package/test/plugins/toobusy.js +186 -9
  68. package/test/plugins/xclient.js +7 -4
  69. package/test/server.js +425 -1
  70. package/test/smtp_client.js +11 -18
  71. package/test/tls_socket.js +3 -6
  72. package/tls_socket.js +3 -3
  73. package/transaction.js +3 -3
  74. package/address.js +0 -53
  75. package/endpoint.js +0 -96
  76. package/host_pool.js +0 -169
  77. package/outbound/fsync_writestream.js +0 -44
  78. package/outbound/timer_queue.js +0 -86
  79. package/rfc1869.js +0 -93
  80. package/test/endpoint.js +0 -128
  81. package/test/host_pool.js +0 -188
  82. package/test/rfc1869.js +0 -89
package/address.js DELETED
@@ -1,53 +0,0 @@
1
- 'use strict'
2
-
3
- // SUNSET 2027: Haraka core constructs envelope addresses with
4
- // @haraka/email-address, whose `.address` / `.host` are string
5
- // properties. Core itself — and a large body of bundled and
6
- // third-party plugins — still use the historical address-rfc2821 /
7
- // address-rfc2822 *method* contract (`addr.address()`, `addr.host()`).
8
- //
9
- // `asLegacy()` wraps each instance so both the new string-property API
10
- // and the legacy callable form work during the transition. The wrap is
11
- // idempotent, and `unwrapLegacy()` recovers the raw instance so the
12
- // outbound queue's JSON re-hydration copies primitive string fields
13
- // rather than the callable accessors.
14
- //
15
- // Once the ecosystem has migrated, delete this module, require
16
- // `Address` straight from '@haraka/email-address', and drop the wrapper
17
- // (see @haraka/email-address lib/legacy.js).
18
-
19
- const { Address: BaseAddress, asLegacy, unwrapLegacy } = require('@haraka/email-address')
20
-
21
- class Address extends BaseAddress {
22
- constructor(...args) {
23
- // never re-hydrate from a wrapped instance — copy raw strings
24
- if (args.length) args[0] = unwrapLegacy(args[0])
25
- // `new Address(user, host)` where `host` is another wrapped
26
- // address's `.host` — the SUNSET-2027 callable accessor is
27
- // `typeof 'function'`, which BaseAddress would mistake for an
28
- // options object. Coerce it back to the primitive string.
29
- if (args.length >= 2 && typeof args[1] === 'function') {
30
- args[1] = String(args[1])
31
- }
32
- super(...args)
33
- return asLegacy(this)
34
- }
35
-
36
- // Preserve the address-rfc2821 wire shape so existing on-disk queue
37
- // files stay byte-compatible across the upgrade and re-hydrate
38
- // unchanged. @haraka/email-address additionally carries
39
- // phrase/comment/group/opts, which are irrelevant to envelope
40
- // addresses and must not leak into the persisted todo. SUNSET 2027.
41
- toJSON() {
42
- const out = {
43
- original: this.original,
44
- original_host: this.original_host,
45
- host: this.host,
46
- user: this.user,
47
- }
48
- if (this.is_utf8) out.is_utf8 = true
49
- return out
50
- }
51
- }
52
-
53
- module.exports = { Address }
package/endpoint.js DELETED
@@ -1,96 +0,0 @@
1
- 'use strict'
2
- // Socket address parser/formatter and server binding helper
3
-
4
- const fs = require('node:fs/promises')
5
- const net = require('node:net')
6
-
7
- function parseSockaddr(addr, defaultPort = 0) {
8
- let match
9
- if (/^[0-9]+$/.test(addr)) return { host: '::', port: parseInt(addr, 10) }
10
-
11
- const lastColon = addr.lastIndexOf(':')
12
- if (lastColon !== -1) {
13
- const host = addr.slice(0, lastColon)
14
- const port = addr.slice(lastColon + 1)
15
-
16
- if (host.includes(':') && /^\d+$/.test(port) && net.isIP(host) === 6) {
17
- return { host: host.toLowerCase(), port: parseInt(port, 10) }
18
- }
19
- }
20
- if (net.isIP(addr) === 6) return { host: addr.toLowerCase(), port: defaultPort }
21
-
22
- if ((match = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/.exec(addr)))
23
- return { host: match[1], port: match[2] !== undefined ? parseInt(match[2], 10) : defaultPort }
24
- if ((match = /^\[([0-9a-fA-F:]+)\](?::(\d+))?$/.exec(addr)))
25
- return { host: match[1].toLowerCase(), port: match[2] !== undefined ? parseInt(match[2], 10) : defaultPort }
26
- if (
27
- (match =
28
- /^([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*)(?::(\d+))?$/.exec(
29
- addr,
30
- ))
31
- )
32
- return { host: match[1].toLowerCase(), port: match[2] !== undefined ? parseInt(match[2], 10) : defaultPort }
33
- if (addr.includes('/')) return { path: addr }
34
- throw new Error(`Invalid socket address ${addr}`)
35
- }
36
-
37
- module.exports = function endpoint(addr, defaultPort) {
38
- try {
39
- if ('string' === typeof addr || 'number' === typeof addr) {
40
- addr = parseSockaddr(addr, defaultPort)
41
- const match = /^(.*):([0-7]{3})$/.exec(addr.path || '')
42
- if (match) {
43
- addr.path = match[1]
44
- addr.mode = match[2]
45
- }
46
- }
47
- } catch (err) {
48
- // Return the parse exception instead of throwing it
49
- return err
50
- }
51
- return new Endpoint(addr)
52
- }
53
-
54
- class Endpoint {
55
- constructor(addr) {
56
- if (addr.path) {
57
- this.path = addr.path
58
- if (addr.mode) this.mode = addr.mode
59
- } else {
60
- // Handle server.address() return as well as parsed host/port
61
- const host = addr.address || addr.host || '::0'
62
- // Normalize '::' to '::0'
63
- this.host = '::' === host ? '::0' : host
64
- this.port = parseInt(addr.port, 10)
65
- }
66
- }
67
-
68
- toString() {
69
- if (this.mode) return `${this.path}:${this.mode}`
70
- if (this.path) return this.path
71
- if (this.host.includes(':')) return `[${this.host}]:${this.port}`
72
- return `${this.host}:${this.port}`
73
- }
74
-
75
- // Make server listen on this endpoint, w/optional options
76
- async bind(server, opts) {
77
- opts = { ...opts }
78
-
79
- const mode = this.mode ? parseInt(this.mode, 8) : false
80
- if (this.path) {
81
- opts.path = this.path
82
- await fs.rm(this.path, { force: true }) // errors are ignored when force is true
83
- } else {
84
- opts.host = this.host
85
- opts.port = this.port
86
- }
87
-
88
- return new Promise((resolve, reject) => {
89
- server.listen(opts, async (err) => {
90
- if (err) return reject(err)
91
- if (mode) await fs.chmod(opts.path, mode)
92
- resolve()
93
- })
94
- })
95
- }
96
- }
package/host_pool.js DELETED
@@ -1,169 +0,0 @@
1
- 'use strict'
2
-
3
- const net = require('node:net')
4
- const utils = require('haraka-utils')
5
-
6
- /* HostPool:
7
- *
8
- * Starts with a pool of backend hosts from a "forwarding_host_pool"
9
- * configuration that looks like this (port defaults to 25 if not set):
10
- *
11
- * 1.1.1.1:11, 2.2.2.2:22, 3.3.3.3:33
12
- *
13
- * It randomizes the list and then gives then out sequentially (for
14
- * predictability).
15
- *
16
- * If failed() is called with one of the hosts, we mark it down for retry_secs
17
- * and don't give it out again until that period has passed.
18
- *
19
- * If *all* the hosts have been marked down, ignore the marks and give
20
- * out the next host. That's to keep a random short-lived but widespread
21
- * network failure from taking the whole system down.
22
- */
23
-
24
- const logger = require('./logger')
25
-
26
- class HostPool {
27
- // takes a comma/space-separated list of ip:ports
28
- // 1.1.1.1:22, 3.3.3.3:44
29
- constructor(hostports_str, retry_secs) {
30
- const hosts = (hostports_str || '')
31
- .trim()
32
- .split(/[\s,]+/)
33
- .map((hostport) => {
34
- const splithost = hostport.split(/:/)
35
- if (!splithost[1]) {
36
- splithost[1] = 25
37
- }
38
- return {
39
- host: splithost[0],
40
- port: splithost[1],
41
- }
42
- })
43
- this.hostports_str = hostports_str
44
- this.hosts = utils.shuffle(hosts)
45
- this.dead_hosts = {} // hostport => true/false
46
- this.last_i = 0 // the last one we checked
47
- this.retry_secs = retry_secs || 10
48
- }
49
-
50
- /* failed
51
- *
52
- * Part of the external API for this module. Call it when you see a failure to
53
- * this backend host and it'll come out of the pool and put into the recheck
54
- * timer.
55
- */
56
- failed(host, port) {
57
- const self = this
58
- const key = `${host}:${port}`
59
- const retry_msecs = self.retry_secs * 1000
60
- self.dead_hosts[key] = true
61
-
62
- function cb_if_still_dead() {
63
- logger.warn(`${host} ${key} is still dead, will retry in ${self.retry_secs} secs`)
64
- self.dead_hosts[key] = true
65
- // console.log(1);
66
- setTimeout(() => {
67
- self.probe_dead_host(host, port, cb_if_still_dead, cb_if_alive)
68
- }, retry_msecs)
69
- }
70
-
71
- function cb_if_alive() {
72
- // console.log(2);
73
- logger.info(`${host} ${key} is back! adding back into pool`)
74
- delete self.dead_hosts[key]
75
- }
76
-
77
- setTimeout(() => {
78
- self.probe_dead_host(host, port, cb_if_still_dead, cb_if_alive)
79
- }, retry_msecs)
80
- }
81
-
82
- /* probe_dead_host
83
- *
84
- * When the timer fires, we'll ping the host, and if it's still dead we'll
85
- * update the dead_hosts list. If it's back online, we just don't touch the
86
- * dead_hosts lists, and the next time get_host() is called, it'll be in the
87
- * mix.
88
- */
89
- probe_dead_host(host, port, cb_if_still_dead, cb_if_alive) {
90
- logger.info(`probing dead host ${host}:${port}`)
91
-
92
- const connect_timeout_ms = 200 // keep it snappy
93
- let s
94
- try {
95
- s = this.get_socket()
96
- s.setTimeout(connect_timeout_ms, () => {
97
- // nobody home, it's still dead
98
- s.destroy()
99
- cb_if_still_dead()
100
- })
101
- s.on('error', (e) => {
102
- // silently catch all errors - assume the port is closed
103
- s.destroy()
104
- cb_if_still_dead()
105
- })
106
-
107
- s.connect(port, host, () => {
108
- cb_if_alive()
109
- s.destroy() // will this conflict with setTimeout's s.destroy?
110
- })
111
- } catch (e) {
112
- // only way to catch run-time javascript errors in here;
113
- console.log(`ERROR in probe_dead_host, got error ${e}`)
114
- throw e
115
- }
116
- }
117
-
118
- /* get_socket
119
- *
120
- * so we can override in unit test
121
- */
122
- get_socket() {
123
- return new net.Socket()
124
- }
125
-
126
- /* get_host
127
- *
128
- * This approach borrowed from the danga mogilefs client code
129
- *
130
- * If all the hosts look dead, it returns the next one it would have tried
131
- * anyway. That should make it more forgiving about transient but widespread
132
- * network problems that make all the hosts look dead.
133
- */
134
- get_host() {
135
- let host
136
- let found
137
-
138
- let first_i = this.last_i + 1
139
- if (first_i >= this.hosts.length) {
140
- first_i = 0
141
- }
142
-
143
- for (let i = 0; i < this.hosts.length; ++i) {
144
- let j = i + first_i
145
- if (j >= this.hosts.length) {
146
- j -= this.hosts.length
147
- }
148
- host = this.hosts[j]
149
- const key = `${host.host}:${host.port}`
150
- if (this.dead_hosts[key]) {
151
- continue
152
- }
153
- this.last_i = j
154
- found = true
155
- break
156
- }
157
- if (found) {
158
- return host
159
- } else {
160
- logger.warn(
161
- `no working hosts found, retrying a dead one, config (probably from smtp_forward.forwarding_host_pool) is '${this.hostports_str}'`,
162
- )
163
- this.last_i = first_i
164
- return this.hosts[first_i]
165
- }
166
- }
167
- }
168
-
169
- module.exports = HostPool
@@ -1,44 +0,0 @@
1
- 'use strict'
2
-
3
- const fs = require('node:fs')
4
-
5
- class FsyncWriteStream extends fs.WriteStream {
6
- constructor(path, options) {
7
- super(path, options)
8
- }
9
-
10
- close(cb) {
11
- const self = this
12
- if (cb) this.once('close', cb)
13
-
14
- if (this.closed || 'number' !== typeof this.fd) {
15
- if ('number' !== typeof this.fd) {
16
- this.once('open', close)
17
- return
18
- }
19
- return setImmediate(this.emit.bind(this, 'close'))
20
- }
21
- this.closed = true
22
- close()
23
-
24
- function close(fd) {
25
- fs.fsync(fd || self.fd, (er) => {
26
- if (er) {
27
- self.emit('error', er)
28
- return
29
- }
30
-
31
- fs.close(fd || self.fd, (err) => {
32
- if (err) {
33
- self.emit('error', err)
34
- } else {
35
- self.emit('close')
36
- }
37
- })
38
- self.fd = null
39
- })
40
- }
41
- }
42
- }
43
-
44
- module.exports = FsyncWriteStream
@@ -1,86 +0,0 @@
1
- 'use strict'
2
-
3
- const logger = require('../logger')
4
-
5
- class TQTimer {
6
- constructor(id, fire_time, cb) {
7
- this.id = id
8
- this.fire_time = fire_time
9
- this.cb = cb
10
- }
11
-
12
- cancel() {
13
- this.cb = null
14
- }
15
- }
16
-
17
- class TimerQueue {
18
- constructor(interval = 1000) {
19
- this.name = 'outbound/timer_queue'
20
- this.queue = []
21
- this.interval_timer = setInterval(() => {
22
- this.fire()
23
- }, interval)
24
- this.interval_timer.unref() // allow server to exit
25
- }
26
-
27
- add(id, ms, cb) {
28
- const fire_time = Date.now() + ms
29
-
30
- const timer = new TQTimer(id, fire_time, cb)
31
-
32
- if (this.queue.length === 0 || fire_time >= this.queue[this.queue.length - 1].fire_time) {
33
- this.queue.push(timer)
34
- return timer
35
- }
36
-
37
- for (let i = 0; i < this.queue.length; i++) {
38
- if (this.queue[i].fire_time > fire_time) {
39
- this.queue.splice(i, 0, timer)
40
- return timer
41
- }
42
- }
43
-
44
- throw 'Should never get here'
45
- }
46
-
47
- discard(id) {
48
- for (let i = 0; i < this.queue.length; i++) {
49
- if (this.queue[i].id === id) {
50
- this.queue[i].cancel()
51
- return this.queue.splice(i, 1)
52
- }
53
- }
54
-
55
- throw `${id} not found`
56
- }
57
-
58
- fire() {
59
- if (this.queue.length === 0) return
60
-
61
- const now = Date.now()
62
-
63
- while (this.queue.length && this.queue[0].fire_time <= now) {
64
- const to_run = this.queue.shift()
65
- if (to_run.cb) to_run.cb()
66
- }
67
- }
68
-
69
- length() {
70
- return this.queue.length
71
- }
72
-
73
- drain() {
74
- logger.debug(this, `Draining ${this.queue.length} items from the queue`)
75
- while (this.queue.length) {
76
- const to_run = this.queue.shift()
77
- if (to_run.cb) to_run.cb()
78
- }
79
- }
80
-
81
- shutdown() {
82
- clearInterval(this.interval_timer)
83
- }
84
- }
85
-
86
- module.exports = TimerQueue
package/rfc1869.js DELETED
@@ -1,93 +0,0 @@
1
- 'use strict'
2
- // RFC 1869 command parser
3
-
4
- // 6. MAIL FROM and RCPT TO Parameters
5
- // [...]
6
- //
7
- // esmtp-cmd ::= inner-esmtp-cmd [SP esmtp-parameters] CR LF
8
- // esmtp-parameters ::= esmtp-parameter *(SP esmtp-parameter)
9
- // esmtp-parameter ::= esmtp-keyword ["=" esmtp-value]
10
- // esmtp-keyword ::= (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
11
- //
12
- // ; syntax and values depend on esmtp-keyword
13
- // esmtp-value ::= 1*<any CHAR excluding "=", SP, and all
14
- // control characters (US ASCII 0-31
15
- // inclusive)>
16
- //
17
- // ; The following commands are extended to
18
- // ; accept extended parameters.
19
- // inner-esmtp-cmd ::= ("MAIL FROM:" reverse-path) /
20
- // ("RCPT TO:" forward-path)
21
-
22
- /* eslint no-control-regex: 0 */
23
- const chew_regexp = /\s+([A-Za-z0-9][A-Za-z0-9-]*(?:=[^= \x00-\x1f]+)?)$/
24
-
25
- exports.parse = (type, line, strict) => {
26
- let params = []
27
- line = new String(line).replace(/\s*$/, '')
28
- if (type === 'mail') {
29
- line = line.replace(strict ? /from:/i : /from:\s*/i, '')
30
- } else {
31
- line = line.replace(strict ? /to:/i : /to:\s*/i, '')
32
- }
33
-
34
- while (1) {
35
- const old_length = line.length
36
- line = line.replace(chew_regexp, (str, p1) => {
37
- params.push(p1)
38
- return ''
39
- })
40
- if (old_length === line.length) break
41
- }
42
-
43
- params = params.reverse()
44
-
45
- // the above will "fail" (i.e. all of the line in params) on
46
- // some addresses without <> like
47
- // MAIL FROM: user=name@example.net
48
- // or RCPT TO: postmaster
49
-
50
- // let's see if $line contains nothing and use the first value as address:
51
- if (line.length) {
52
- // parameter syntax error, i.e. not all of the arguments were
53
- // stripped by the while() loop:
54
- if (line.match(/@.*\s/)) {
55
- throw new Error(`Syntax error in parameters ("${line}")`)
56
- }
57
-
58
- params.unshift(line)
59
- }
60
-
61
- line = params.shift() || ''
62
- if (strict) {
63
- if (!line.match(/^<.*>$/)) {
64
- throw new Error(`Invalid format of ${type} command: ${line}`)
65
- }
66
- }
67
-
68
- if (type === 'mail') {
69
- if (!line.length) {
70
- return ['<>'] // 'MAIL FROM:' --> 'MAIL FROM:<>'
71
- }
72
- if (line.match(/@.*\s/)) {
73
- throw new Error('Syntax error in parameters')
74
- }
75
- } else {
76
- // console.log(`Looking at ${line}`);
77
- if (line.match(/@.*\s/)) {
78
- throw new Error('Syntax error in parameters')
79
- }
80
- if (line.match(/\s/)) {
81
- throw new Error('Syntax error in parameters')
82
- }
83
- if (line.match(/@/)) {
84
- if (!line.match(/^<.*>$/)) line = `<${line}>`
85
- } else if (!line.match(/^<(postmaster|abuse)>$/i)) {
86
- throw new Error(`Syntax error in address: ${line}`)
87
- }
88
- }
89
-
90
- params.unshift(line)
91
-
92
- return params
93
- }
package/test/endpoint.js DELETED
@@ -1,128 +0,0 @@
1
- 'use strict'
2
-
3
- const { describe, it, beforeEach, afterEach } = require('node:test')
4
- const assert = require('node:assert')
5
-
6
- const mock = require('mock-require')
7
- const endpoint = require('../endpoint')
8
-
9
- describe('endpoint', () => {
10
- it('toString()', () => {
11
- assert.equal(endpoint(25), '[::0]:25')
12
- assert.equal(endpoint('10.0.0.3', 42), '10.0.0.3:42')
13
- assert.equal(endpoint('/foo/bar.sock'), '/foo/bar.sock')
14
- assert.equal(endpoint('/foo/bar.sock:770'), '/foo/bar.sock:770')
15
- assert.equal(endpoint({ address: '::0', port: 80 }), '[::0]:80')
16
- })
17
-
18
- describe('parse', () => {
19
- it('Number as port', () => {
20
- assert.deepEqual(endpoint(25), { host: '::0', port: 25 })
21
- })
22
-
23
- it('Unbracketed IPv6 host uses default port', () => {
24
- assert.deepEqual(endpoint('::0', 25), {
25
- host: '::0',
26
- port: 25,
27
- })
28
- })
29
-
30
- it('Unbracketed IPv6 host:port parses correctly (PR #3552 compatibility)', () => {
31
- assert.deepEqual(endpoint('::0:25'), {
32
- host: '::0',
33
- port: 25,
34
- })
35
- })
36
-
37
- it('Default port if only host', () => {
38
- assert.deepEqual(endpoint('10.0.0.3', 42), {
39
- host: '10.0.0.3',
40
- port: 42,
41
- })
42
- })
43
-
44
- it('Bracketed IPv6 host is normalized to lowercase', () => {
45
- assert.deepEqual(endpoint('[ABCD::EF01]:2525'), {
46
- host: 'abcd::ef01',
47
- port: 2525,
48
- })
49
- })
50
-
51
- it('Unix socket', () => {
52
- assert.deepEqual(endpoint('/foo/bar.sock'), {
53
- path: '/foo/bar.sock',
54
- })
55
- })
56
-
57
- it('Unix socket w/mode', () => {
58
- assert.deepEqual(endpoint('/foo/bar.sock:770'), {
59
- path: '/foo/bar.sock',
60
- mode: '770',
61
- })
62
- })
63
-
64
- it('Invalid unbracketed IPv6 host with non-numeric tail returns Error', () => {
65
- const ep = endpoint('::0:port')
66
- assert.equal(ep instanceof Error, true)
67
- assert.match(ep.message, /Invalid socket address/)
68
- })
69
- })
70
-
71
- describe('bind()', () => {
72
- beforeEach(() => {
73
- // Mock filesystem and log server + fs method calls
74
- const modes = (this.modes = {})
75
- const log = (this.log = [])
76
-
77
- this.server = {
78
- listen(opts, cb) {
79
- log.push(['listen', opts])
80
- if (cb) cb()
81
- },
82
- }
83
-
84
- this.mockfs = {
85
- chmod(path, mode, ...args) {
86
- log.push(['chmod', path, mode, ...args])
87
- modes[path] = mode
88
- },
89
- rm(path, ...args) {
90
- log.push(['rm', path, ...args])
91
- },
92
- }
93
-
94
- mock('node:fs/promises', this.mockfs)
95
- this.endpoint = mock.reRequire('../endpoint')
96
- })
97
-
98
- afterEach(() => {
99
- mock.stop('node:fs/promises')
100
- })
101
-
102
- it('IP socket', async () => {
103
- await this.endpoint('10.0.0.3:42').bind(this.server, {
104
- backlog: 19,
105
- })
106
- assert.deepEqual(this.log, [['listen', { host: '10.0.0.3', port: 42, backlog: 19 }]])
107
- })
108
-
109
- it('Unix socket', async () => {
110
- await this.endpoint('/foo/bar.sock').bind(this.server, {
111
- readableAll: true,
112
- })
113
- assert.deepEqual(this.log, [
114
- ['rm', '/foo/bar.sock', { force: true }],
115
- ['listen', { path: '/foo/bar.sock', readableAll: true }],
116
- ])
117
- })
118
-
119
- it('Unix socket w/mode', async () => {
120
- await this.endpoint('/foo/bar.sock:764').bind(this.server)
121
- assert.deepEqual(this.log, [
122
- ['rm', '/foo/bar.sock', { force: true }],
123
- ['listen', { path: '/foo/bar.sock' }],
124
- ['chmod', '/foo/bar.sock', 0o764],
125
- ])
126
- })
127
- })
128
- })