haraka 0.0.32 → 3.3.0

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 (310) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/.githooks/pre-commit +41 -0
  3. package/.prettierignore +6 -0
  4. package/.qlty/.gitignore +7 -0
  5. package/.qlty/configs/.shellcheckrc +1 -0
  6. package/.qlty/qlty.toml +15 -0
  7. package/CHANGELOG.md +1872 -62
  8. package/CLAUDE.md +40 -0
  9. package/CONTRIBUTORS.md +34 -0
  10. package/Dockerfile +50 -0
  11. package/GEMINI.md +38 -0
  12. package/LICENSE +2 -1
  13. package/Plugins.md +227 -0
  14. package/README.md +100 -115
  15. package/SECURITY.md +178 -0
  16. package/TODO +22 -0
  17. package/address.js +53 -0
  18. package/bin/haraka +593 -0
  19. package/bin/haraka_grep +32 -0
  20. package/config/aliases +2 -0
  21. package/config/auth_flat_file.ini +7 -0
  22. package/config/auth_vpopmaild.ini +9 -0
  23. package/config/connection.ini +79 -0
  24. package/config/delay_deny.ini +7 -0
  25. package/config/dhparams.pem +8 -0
  26. package/config/host_list +3 -0
  27. package/config/host_list_regex +6 -0
  28. package/config/http.ini +11 -0
  29. package/config/lmtp.ini +7 -0
  30. package/config/log.ini +11 -0
  31. package/config/me +1 -0
  32. package/config/outbound.bounce_message +18 -0
  33. package/config/outbound.bounce_message_html +36 -0
  34. package/config/outbound.bounce_message_image +106 -0
  35. package/config/outbound.ini +24 -0
  36. package/config/plugins +67 -0
  37. package/config/smtp.ini +37 -0
  38. package/config/smtp_bridge.ini +4 -0
  39. package/config/smtp_forward.ini +31 -0
  40. package/config/smtp_proxy.ini +27 -0
  41. package/config/tarpit.timeout +1 -0
  42. package/config/tls.ini +83 -0
  43. package/config/tls_cert.pem +23 -0
  44. package/config/tls_key.pem +28 -0
  45. package/config/watch.ini +12 -0
  46. package/config/xclient.hosts +2 -0
  47. package/connection.js +1863 -0
  48. package/contrib/Haraka.cf +6 -0
  49. package/contrib/Haraka.pm +35 -0
  50. package/contrib/bad_smtp_server.pl +25 -0
  51. package/contrib/bsd-rc.d/haraka +61 -0
  52. package/contrib/debian-init.d/haraka +87 -0
  53. package/contrib/haraka.init +96 -0
  54. package/contrib/haraka.service +23 -0
  55. package/contrib/plugin2npm.sh +81 -0
  56. package/contrib/ubuntu-upstart/haraka.conf +27 -0
  57. package/coverage/coverage-final.json +2 -0
  58. package/coverage/coverage-summary.json +33 -0
  59. package/coverage/tmp/coverage-79131-1779241025146-0.json +1 -0
  60. package/coverage/tmp/coverage-79132-1779240999690-0.json +1 -0
  61. package/coverage/tmp/coverage-79172-1779241000095-0.json +1 -0
  62. package/coverage/tmp/coverage-79210-1779241000156-0.json +1 -0
  63. package/coverage/tmp/coverage-79211-1779241000209-0.json +1 -0
  64. package/coverage/tmp/coverage-79212-1779241000266-0.json +1 -0
  65. package/coverage/tmp/coverage-79213-1779241000441-0.json +1 -0
  66. package/coverage/tmp/coverage-79214-1779241000626-0.json +1 -0
  67. package/coverage/tmp/coverage-79215-1779241000795-0.json +1 -0
  68. package/coverage/tmp/coverage-79216-1779241000965-0.json +1 -0
  69. package/coverage/tmp/coverage-79218-1779241001013-0.json +1 -0
  70. package/coverage/tmp/coverage-79219-1779241001179-0.json +1 -0
  71. package/coverage/tmp/coverage-79220-1779241006249-0.json +1 -0
  72. package/coverage/tmp/coverage-79227-1779241011453-0.json +1 -0
  73. package/coverage/tmp/coverage-79229-1779241011537-0.json +1 -0
  74. package/coverage/tmp/coverage-79230-1779241011647-0.json +1 -0
  75. package/coverage/tmp/coverage-79231-1779241011765-0.json +1 -0
  76. package/coverage/tmp/coverage-79232-1779241011841-0.json +1 -0
  77. package/coverage/tmp/coverage-79233-1779241011909-0.json +1 -0
  78. package/coverage/tmp/coverage-79234-1779241011984-0.json +1 -0
  79. package/coverage/tmp/coverage-79235-1779241012055-0.json +1 -0
  80. package/coverage/tmp/coverage-79236-1779241012230-0.json +1 -0
  81. package/coverage/tmp/coverage-79237-1779241012300-0.json +1 -0
  82. package/coverage/tmp/coverage-79238-1779241012368-0.json +1 -0
  83. package/coverage/tmp/coverage-79239-1779241012438-0.json +1 -0
  84. package/coverage/tmp/coverage-79240-1779241012511-0.json +1 -0
  85. package/coverage/tmp/coverage-79241-1779241012582-0.json +1 -0
  86. package/coverage/tmp/coverage-79242-1779241012652-0.json +1 -0
  87. package/coverage/tmp/coverage-79243-1779241012814-0.json +1 -0
  88. package/coverage/tmp/coverage-79244-1779241012931-0.json +1 -0
  89. package/coverage/tmp/coverage-79245-1779241013007-0.json +1 -0
  90. package/coverage/tmp/coverage-79246-1779241013106-0.json +1 -0
  91. package/coverage/tmp/coverage-79247-1779241013178-0.json +1 -0
  92. package/coverage/tmp/coverage-79248-1779241013244-0.json +1 -0
  93. package/coverage/tmp/coverage-79249-1779241013409-0.json +1 -0
  94. package/coverage/tmp/coverage-79250-1779241013697-0.json +1 -0
  95. package/coverage/tmp/coverage-79251-1779241013847-0.json +1 -0
  96. package/coverage/tmp/coverage-79252-1779241014288-0.json +1 -0
  97. package/coverage/tmp/coverage-79253-1779241014378-0.json +1 -0
  98. package/coverage/tmp/coverage-79254-1779241014428-0.json +1 -0
  99. package/coverage/tmp/coverage-79255-1779241021774-0.json +1 -0
  100. package/coverage/tmp/coverage-80382-1779241021949-0.json +1 -0
  101. package/coverage/tmp/coverage-80383-1779241025019-0.json +1 -0
  102. package/coverage/tmp/coverage-80384-1779241025133-0.json +1 -0
  103. package/docs/Body.md +1 -0
  104. package/docs/Config.md +1 -0
  105. package/docs/Connection.md +153 -0
  106. package/docs/CoreConfig.md +96 -0
  107. package/docs/CustomReturnCodes.md +3 -0
  108. package/docs/HAProxy.md +62 -0
  109. package/docs/Header.md +1 -0
  110. package/docs/Logging.md +129 -0
  111. package/docs/Outbound.md +210 -0
  112. package/docs/Plugins.md +372 -0
  113. package/docs/Results.md +7 -0
  114. package/docs/Transaction.md +135 -0
  115. package/docs/Tutorial.md +183 -0
  116. package/docs/deprecated/access.md +3 -0
  117. package/docs/deprecated/backscatterer.md +9 -0
  118. package/docs/deprecated/connect.rdns_access.md +53 -0
  119. package/docs/deprecated/data.headers.md +3 -0
  120. package/docs/deprecated/data.nomsgid.md +7 -0
  121. package/docs/deprecated/data.noreceived.md +11 -0
  122. package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
  123. package/docs/deprecated/dkim_sign.md +97 -0
  124. package/docs/deprecated/dkim_verify.md +28 -0
  125. package/docs/deprecated/dnsbl.md +80 -0
  126. package/docs/deprecated/dnswl.md +73 -0
  127. package/docs/deprecated/lookup_rdns.strict.md +67 -0
  128. package/docs/deprecated/mail_from.access.md +52 -0
  129. package/docs/deprecated/mail_from.blocklist.md +18 -0
  130. package/docs/deprecated/mail_from.nobounces.md +8 -0
  131. package/docs/deprecated/rcpt_to.access.md +53 -0
  132. package/docs/deprecated/rcpt_to.blocklist.md +18 -0
  133. package/docs/deprecated/rcpt_to.routes.md +3 -0
  134. package/docs/deprecated/rdns.regexp.md +30 -0
  135. package/docs/plugins/aliases.md +3 -0
  136. package/docs/plugins/auth/auth_bridge.md +34 -0
  137. package/docs/plugins/auth/auth_ldap.md +4 -0
  138. package/docs/plugins/auth/auth_proxy.md +36 -0
  139. package/docs/plugins/auth/auth_vpopmaild.md +33 -0
  140. package/docs/plugins/auth/flat_file.md +40 -0
  141. package/docs/plugins/block_me.md +18 -0
  142. package/docs/plugins/data.signatures.md +11 -0
  143. package/docs/plugins/delay_deny.md +23 -0
  144. package/docs/plugins/max_unrecognized_commands.md +6 -0
  145. package/docs/plugins/prevent_credential_leaks.md +22 -0
  146. package/docs/plugins/process_title.md +42 -0
  147. package/docs/plugins/queue/deliver.md +3 -0
  148. package/docs/plugins/queue/discard.md +32 -0
  149. package/docs/plugins/queue/lmtp.md +24 -0
  150. package/docs/plugins/queue/qmail-queue.md +16 -0
  151. package/docs/plugins/queue/quarantine.md +87 -0
  152. package/docs/plugins/queue/smtp_bridge.md +32 -0
  153. package/docs/plugins/queue/smtp_forward.md +127 -0
  154. package/docs/plugins/queue/smtp_proxy.md +68 -0
  155. package/docs/plugins/queue/test.md +7 -0
  156. package/docs/plugins/rcpt_to.in_host_list.md +34 -0
  157. package/docs/plugins/rcpt_to.max_count.md +3 -0
  158. package/docs/plugins/record_envelope_addresses.md +20 -0
  159. package/docs/plugins/relay.md +3 -0
  160. package/docs/plugins/reseed_rng.md +16 -0
  161. package/docs/plugins/status.md +41 -0
  162. package/docs/plugins/tarpit.md +50 -0
  163. package/docs/plugins/tls.md +235 -0
  164. package/docs/plugins/toobusy.md +27 -0
  165. package/docs/plugins/xclient.md +10 -0
  166. package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
  167. package/docs/tutorials/SettingUpOutbound.md +62 -0
  168. package/eslint.config.mjs +2 -0
  169. package/haraka.js +74 -0
  170. package/haraka.sh +2 -0
  171. package/http/html/404.html +58 -0
  172. package/http/html/index.html +47 -0
  173. package/http/package.json +21 -0
  174. package/line_socket.js +24 -0
  175. package/logger.js +322 -0
  176. package/outbound/client_pool.js +59 -0
  177. package/outbound/config.js +134 -0
  178. package/outbound/hmail.js +1504 -0
  179. package/outbound/index.js +349 -0
  180. package/outbound/qfile.js +93 -0
  181. package/outbound/queue.js +399 -0
  182. package/outbound/tls.js +85 -0
  183. package/outbound/todo.js +17 -0
  184. package/package.json +91 -29
  185. package/plugins/.eslintrc.yaml +3 -0
  186. package/plugins/auth/auth_base.js +261 -0
  187. package/plugins/auth/auth_bridge.js +20 -0
  188. package/plugins/auth/auth_proxy.js +227 -0
  189. package/plugins/auth/auth_vpopmaild.js +162 -0
  190. package/plugins/auth/flat_file.js +44 -0
  191. package/plugins/block_me.js +88 -0
  192. package/plugins/data.signatures.js +30 -0
  193. package/plugins/delay_deny.js +153 -0
  194. package/plugins/prevent_credential_leaks.js +61 -0
  195. package/plugins/process_title.js +197 -0
  196. package/plugins/profile.js +11 -0
  197. package/plugins/queue/deliver.js +12 -0
  198. package/plugins/queue/discard.js +27 -0
  199. package/plugins/queue/lmtp.js +45 -0
  200. package/plugins/queue/qmail-queue.js +93 -0
  201. package/plugins/queue/quarantine.js +133 -0
  202. package/plugins/queue/smtp_bridge.js +45 -0
  203. package/plugins/queue/smtp_forward.js +371 -0
  204. package/plugins/queue/smtp_proxy.js +142 -0
  205. package/plugins/queue/test.js +15 -0
  206. package/plugins/rcpt_to.host_list_base.js +65 -0
  207. package/plugins/rcpt_to.in_host_list.js +56 -0
  208. package/plugins/record_envelope_addresses.js +17 -0
  209. package/plugins/reseed_rng.js +7 -0
  210. package/plugins/status.js +274 -0
  211. package/plugins/tarpit.js +45 -0
  212. package/plugins/tls.js +164 -0
  213. package/plugins/toobusy.js +47 -0
  214. package/plugins/xclient.js +124 -0
  215. package/plugins.js +604 -0
  216. package/queue/1772642154987_1775581346001_4_82235_TGwgfd_2_mattbook-m3.home.simerson.net +0 -0
  217. package/run_tests +11 -0
  218. package/server.js +827 -0
  219. package/smtp_client.js +504 -0
  220. package/test/.eslintrc.yaml +11 -0
  221. package/test/config/auth_flat_file.ini +5 -0
  222. package/test/config/block_me.recipient +1 -0
  223. package/test/config/block_me.senders +1 -0
  224. package/test/config/dhparams.pem +8 -0
  225. package/test/config/host_list +2 -0
  226. package/test/config/outbound_tls_cert.pem +1 -0
  227. package/test/config/outbound_tls_key.pem +1 -0
  228. package/test/config/plugins +7 -0
  229. package/test/config/smtp.ini +11 -0
  230. package/test/config/smtp_forward.ini +30 -0
  231. package/test/config/tls/example.com/_.example.com.key +28 -0
  232. package/test/config/tls/example.com/example.com.crt +25 -0
  233. package/test/config/tls/haraka.local.pem +51 -0
  234. package/test/config/tls.ini +45 -0
  235. package/test/config/tls_cert.pem +21 -0
  236. package/test/config/tls_key.pem +28 -0
  237. package/test/connection.js +817 -0
  238. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  239. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  240. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  241. package/test/fixtures/line_socket.js +21 -0
  242. package/test/fixtures/todo_qfile.txt +0 -0
  243. package/test/fixtures/util_hmailitem.js +156 -0
  244. package/test/installation/config/test-plugin-flat +1 -0
  245. package/test/installation/config/test-plugin.ini +10 -0
  246. package/test/installation/config/tls.ini +1 -0
  247. package/test/installation/node_modules/load_first/index.js +5 -0
  248. package/test/installation/node_modules/load_first/package.json +11 -0
  249. package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
  250. package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
  251. package/test/installation/node_modules/test-plugin/package.json +5 -0
  252. package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
  253. package/test/installation/plugins/base_plugin.js +3 -0
  254. package/test/installation/plugins/folder_plugin/index.js +3 -0
  255. package/test/installation/plugins/folder_plugin/package.json +11 -0
  256. package/test/installation/plugins/inherits.js +7 -0
  257. package/test/installation/plugins/load_first.js +3 -0
  258. package/test/installation/plugins/plugin.js +1 -0
  259. package/test/installation/plugins/tls.js +3 -0
  260. package/test/logger.js +217 -0
  261. package/test/loud/config/dhparams.pem +0 -0
  262. package/test/loud/config/tls/goobered.pem +45 -0
  263. package/test/loud/config/tls.ini +43 -0
  264. package/test/mail_specimen/base64-root-part.txt +23 -0
  265. package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
  266. package/test/outbound/bounce_net_errors.js +133 -0
  267. package/test/outbound/bounce_rfc3464.js +226 -0
  268. package/test/outbound/hmail.js +210 -0
  269. package/test/outbound/index.js +385 -0
  270. package/test/outbound/qfile.js +124 -0
  271. package/test/outbound/queue.js +325 -0
  272. package/test/plugins/auth/auth_base.js +620 -0
  273. package/test/plugins/auth/auth_bridge.js +80 -0
  274. package/test/plugins/auth/auth_vpopmaild.js +81 -0
  275. package/test/plugins/auth/flat_file.js +123 -0
  276. package/test/plugins/block_me.js +141 -0
  277. package/test/plugins/data.signatures.js +111 -0
  278. package/test/plugins/delay_deny.js +262 -0
  279. package/test/plugins/prevent_credential_leaks.js +174 -0
  280. package/test/plugins/process_title.js +141 -0
  281. package/test/plugins/queue/deliver.js +98 -0
  282. package/test/plugins/queue/discard.js +78 -0
  283. package/test/plugins/queue/lmtp.js +137 -0
  284. package/test/plugins/queue/qmail-queue.js +98 -0
  285. package/test/plugins/queue/quarantine.js +80 -0
  286. package/test/plugins/queue/smtp_bridge.js +152 -0
  287. package/test/plugins/queue/smtp_forward.js +1023 -0
  288. package/test/plugins/queue/smtp_proxy.js +138 -0
  289. package/test/plugins/rcpt_to.host_list_base.js +102 -0
  290. package/test/plugins/rcpt_to.in_host_list.js +186 -0
  291. package/test/plugins/record_envelope_addresses.js +66 -0
  292. package/test/plugins/reseed_rng.js +34 -0
  293. package/test/plugins/status.js +207 -0
  294. package/test/plugins/tarpit.js +90 -0
  295. package/test/plugins/tls.js +86 -0
  296. package/test/plugins/toobusy.js +21 -0
  297. package/test/plugins/xclient.js +119 -0
  298. package/test/plugins.js +230 -0
  299. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
  300. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
  301. package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
  302. package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
  303. package/test/queue/zero-length +0 -0
  304. package/test/server.js +1012 -0
  305. package/test/smtp_client.js +1303 -0
  306. package/test/tls_socket.js +321 -0
  307. package/test/transaction.js +554 -0
  308. package/tls_socket.js +771 -0
  309. package/transaction.js +267 -0
  310. package/lib/index.js +0 -371
package/tls_socket.js ADDED
@@ -0,0 +1,771 @@
1
+ 'use strict'
2
+
3
+ const cluster = require('node:cluster')
4
+ const net = require('node:net')
5
+ const path = require('node:path')
6
+ const { spawn } = require('node:child_process')
7
+ const stream = require('node:stream')
8
+ const tls = require('node:tls')
9
+ const util = require('node:util')
10
+
11
+ // npm packages
12
+ exports.config = require('haraka-config') // exported for tests
13
+ const Notes = require('haraka-notes')
14
+
15
+ const log = require('./logger')
16
+
17
+ const certsByHost = new Notes()
18
+ const ctxByHost = {}
19
+ let ocsp
20
+ let ocspCache
21
+
22
+ // provides a common socket for attaching
23
+ // and detaching from either main socket, or crypto socket
24
+ class pluggableStream extends stream.Stream {
25
+ constructor(socket) {
26
+ super()
27
+ this.readable = this.writable = true
28
+ this._timeout = 0
29
+ this._keepalive = false
30
+ this._writeState = true
31
+ this._pending = []
32
+ this._pendingCallbacks = []
33
+ if (socket) this.attach(socket)
34
+ }
35
+
36
+ pause() {
37
+ if (this.targetsocket.pause) {
38
+ this.targetsocket.pause()
39
+ this.readable = false
40
+ }
41
+ }
42
+
43
+ resume() {
44
+ if (this.targetsocket.resume) {
45
+ this.readable = true
46
+ this.targetsocket.resume()
47
+ }
48
+ }
49
+
50
+ attach(socket) {
51
+ this.targetsocket = socket
52
+ this.targetsocket.on('data', (data) => {
53
+ this.emit('data', data)
54
+ })
55
+ this.targetsocket.on('connect', (a, b) => {
56
+ this.emit('connect', a, b)
57
+ })
58
+ this.targetsocket.on('secureConnect', (a, b) => {
59
+ this.emit('secureConnect', a, b)
60
+ this.emit('secure', a, b)
61
+ })
62
+ this.targetsocket.on('secure', (a, b) => {
63
+ this.emit('secure', a, b)
64
+ })
65
+ this.targetsocket.on('end', () => {
66
+ this.writable = this.targetsocket.writable
67
+ this.emit('end')
68
+ })
69
+ this.targetsocket.on('close', (had_error) => {
70
+ this.writable = this.targetsocket.writable
71
+ this.emit('close', had_error)
72
+ })
73
+ this.targetsocket.on('drain', () => {
74
+ this.emit('drain')
75
+ })
76
+ this.targetsocket.once('error', (exception) => {
77
+ this.writable = this.targetsocket.writable
78
+ exception.source = 'tls'
79
+ if (this.listenerCount('error') > 0) this.emit('error', exception)
80
+ })
81
+ this.targetsocket.on('timeout', () => {
82
+ this.emit('timeout')
83
+ })
84
+ if (this.targetsocket.remotePort) {
85
+ this.remotePort = this.targetsocket.remotePort
86
+ }
87
+ if (this.targetsocket.remoteAddress) {
88
+ this.remoteAddress = this.targetsocket.remoteAddress
89
+ }
90
+ if (this.targetsocket.localPort) {
91
+ this.localPort = this.targetsocket.localPort
92
+ }
93
+ if (this.targetsocket.localAddress) {
94
+ this.localAddress = this.targetsocket.localAddress
95
+ }
96
+ }
97
+
98
+ clean() {
99
+ if (this.targetsocket?.removeAllListeners) {
100
+ for (const name of ['data', 'secure', 'secureConnect', 'end', 'close', 'error', 'drain']) {
101
+ this.targetsocket.removeAllListeners(name)
102
+ }
103
+ }
104
+ this.targetsocket = {}
105
+ this.targetsocket.write = () => {}
106
+ }
107
+
108
+ write(data, encoding, callback) {
109
+ if (this.targetsocket.write) {
110
+ return this.targetsocket.write(data, encoding, callback)
111
+ }
112
+ return false
113
+ }
114
+
115
+ end(data, encoding) {
116
+ if (this.targetsocket.end) {
117
+ return this.targetsocket.end(data, encoding)
118
+ }
119
+ }
120
+
121
+ destroySoon() {
122
+ if (this.targetsocket.destroySoon) {
123
+ return this.targetsocket.destroySoon()
124
+ }
125
+ }
126
+
127
+ destroy() {
128
+ if (this.targetsocket.destroy) {
129
+ return this.targetsocket.destroy()
130
+ }
131
+ }
132
+
133
+ setKeepAlive(bool) {
134
+ this._keepalive = bool
135
+ return this.targetsocket.setKeepAlive(bool)
136
+ }
137
+
138
+ setNoDelay(/* true||false */) {}
139
+
140
+ unref() {
141
+ return this.targetsocket.unref()
142
+ }
143
+
144
+ setTimeout(timeout) {
145
+ this._timeout = timeout
146
+ return this.targetsocket.setTimeout(timeout)
147
+ }
148
+
149
+ isEncrypted() {
150
+ return this.targetsocket.encrypted
151
+ }
152
+
153
+ isSecure() {
154
+ return this.targetsocket.encrypted && this.targetsocket.authorized
155
+ }
156
+ }
157
+
158
+ exports.parse_x509 = async (string) => {
159
+ const res = {}
160
+ if (!string) return res
161
+
162
+ const keyRe = /([-]+BEGIN (?:\w+ )?PRIVATE KEY[-]+[^-]*[-]+END (?:\w+ )?PRIVATE KEY[-]+)/gm
163
+ res.keys = string.match(keyRe)
164
+
165
+ const certRe = /([-]+BEGIN CERTIFICATE[-]+[^-]*[-]+END CERTIFICATE[-]+)/gm
166
+ res.chain = string.match(certRe)
167
+
168
+ if (res.chain?.length) {
169
+ // it's cleaner to call openssl with each of -enddate, -subject, etc, but it costs
170
+ // 40-50ms per spawn with node v21 on a M1 MBP
171
+ const raw = await openssl(res.chain[0], 'x509', '-noout', '-enddate', '-subject', '-ext', 'subjectAltName')
172
+ if (!raw) return res
173
+
174
+ res.expire = new Date(raw.match(/notAfter=(.* [A-Z]{3})/)[1])
175
+
176
+ const match = /CN\s*=\s*([^/\s,]+)/.exec(raw)
177
+ if (match && match[1]) res.names = [match[1]]
178
+
179
+ for (let name of Array.from(raw.matchAll(/DNS:([^\s,]+)/gm), (m) => m[0])) {
180
+ name = name.replace('DNS:', '')
181
+ if (!res.names.includes(name)) res.names.push(name)
182
+ }
183
+ }
184
+
185
+ return res
186
+ }
187
+
188
+ exports.load_tls_ini = (opts) => {
189
+ log.info('loading tls.ini')
190
+
191
+ const cfg = exports.config.get(
192
+ 'tls.ini',
193
+ {
194
+ booleans: [
195
+ '-redis.disable_for_failed_hosts',
196
+
197
+ // wildcards match in any section and are not initialized
198
+ '*.requestCert',
199
+ '*.rejectUnauthorized',
200
+ '*.honorCipherOrder',
201
+ '*.enableOCSPStapling',
202
+ '*.requestOCSP',
203
+
204
+ // explicitely declared booleans are initialized
205
+ '+main.requestCert',
206
+ '-main.rejectUnauthorized',
207
+ '+main.honorCipherOrder',
208
+ '-main.requestOCSP',
209
+ '-main.mutual_tls',
210
+ ],
211
+ },
212
+ () => {
213
+ this.load_tls_ini()
214
+ },
215
+ )
216
+
217
+ if (cfg.no_tls_hosts === undefined) cfg.no_tls_hosts = {}
218
+ if (cfg.mutual_auth_hosts === undefined) cfg.mutual_auth_hosts = {}
219
+ if (cfg.mutual_auth_hosts_exclude === undefined) cfg.mutual_auth_hosts_exclude = {}
220
+
221
+ if (cfg.main.enableOCSPStapling !== undefined) {
222
+ log.error('deprecated setting enableOCSPStapling in tls.ini')
223
+ cfg.main.requestOCSP = cfg.main.enableOCSPStapling
224
+ }
225
+
226
+ if (ocsp === undefined && cfg.main.requestOCSP) {
227
+ try {
228
+ ocsp = require('@haraka/ocsp')
229
+ log.debug('ocsp loaded')
230
+ ocspCache = new ocsp.Cache()
231
+ } catch (ignore) {
232
+ log.notice('OCSP Stapling not available.')
233
+ }
234
+ }
235
+
236
+ if (cfg.main.requireAuthorized === undefined) {
237
+ cfg.main.requireAuthorized = []
238
+ } else if (!Array.isArray(cfg.main.requireAuthorized)) {
239
+ cfg.main.requireAuthorized = [cfg.main.requireAuthorized]
240
+ }
241
+
242
+ if (!Array.isArray(cfg.main.no_starttls_ports)) cfg.main.no_starttls_ports = []
243
+
244
+ this.cfg = cfg
245
+
246
+ if (!opts || opts.role === 'server') {
247
+ this.applySocketOpts('*')
248
+ this.load_default_opts()
249
+ }
250
+
251
+ return cfg
252
+ }
253
+
254
+ // Build a client tls_options, merges a consumers own [tls] section
255
+ // over tls.ini [main].
256
+ exports.load_plugin_tls_options = (plugin_tls_cfg = {}) => {
257
+ const tls_cfg = exports.load_tls_ini({ role: 'client' })
258
+ const cfg = JSON.parse(JSON.stringify(plugin_tls_cfg))
259
+
260
+ // Inheritance from tls.ini [main] deliberately omits no_tls_hosts: the
261
+ // [main].no_tls_hosts list is documented as inbound-only; outbound and
262
+ // queue plugins should opt in explicitly via their own section.
263
+ const inheritable_opts = [
264
+ 'key',
265
+ 'cert',
266
+ 'ciphers',
267
+ 'minVersion',
268
+ 'dhparam',
269
+ 'requestCert',
270
+ 'honorCipherOrder',
271
+ 'rejectUnauthorized',
272
+ 'force_tls_hosts',
273
+ ]
274
+ for (const opt of inheritable_opts) {
275
+ if (cfg[opt] !== undefined) continue // set in plugin [tls]
276
+ if (tls_cfg.main[opt] === undefined) continue // unset in tls.ini [main]
277
+ cfg[opt] = tls_cfg.main[opt]
278
+ }
279
+
280
+ // Resolve key/cert/dhparam file references to buffers. Drop empty results
281
+ // so we never pass null to tls.connect.
282
+ for (const k of ['key', 'cert', 'dhparam']) {
283
+ if (!cfg[k]) {
284
+ delete cfg[k]
285
+ continue
286
+ }
287
+ const ref = Array.isArray(cfg[k]) ? cfg[k][0] : cfg[k]
288
+ const bin = exports.config.get(ref, 'binary')
289
+ if (bin) cfg[k] = bin
290
+ else delete cfg[k]
291
+ }
292
+
293
+ for (const k of ['no_tls_hosts', 'force_tls_hosts']) {
294
+ if (!cfg[k]) {
295
+ cfg[k] = []
296
+ continue
297
+ }
298
+ if (!Array.isArray(cfg[k])) cfg[k] = [cfg[k]]
299
+ }
300
+
301
+ return cfg
302
+ }
303
+
304
+ exports.applySocketOpts = (name) => {
305
+ // https://nodejs.org/api/tls.html#tls_new_tls_tlssocket_socket_options
306
+ const TLSSocketOptions = [
307
+ // 'server' // manually added
308
+ 'isServer',
309
+ 'requestCert',
310
+ 'rejectUnauthorized',
311
+ 'NPNProtocols',
312
+ 'ALPNProtocols',
313
+ 'session',
314
+ 'requestOCSP',
315
+ 'secureContext',
316
+ 'SNICallback',
317
+ ]
318
+
319
+ // https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
320
+ const createSecureContextOptions = [
321
+ 'key',
322
+ 'cert',
323
+ 'dhparam',
324
+ 'pfx',
325
+ 'passphrase',
326
+ 'ca',
327
+ 'crl',
328
+ 'ciphers',
329
+ 'minVersion',
330
+ 'honorCipherOrder',
331
+ 'ecdhCurve',
332
+ 'secureProtocol',
333
+ 'secureOptions',
334
+ 'sessionIdContext',
335
+ ]
336
+
337
+ for (const opt of [...TLSSocketOptions, ...createSecureContextOptions]) {
338
+ if (this.cfg[name] && this.cfg[name][opt] !== undefined) {
339
+ // if the setting exists in tls.ini [name]
340
+ certsByHost.set([name, opt], this.cfg[name][opt])
341
+ } else if (this.cfg.main[opt] !== undefined) {
342
+ // save settings in tls.ini [main] to each CN
343
+ certsByHost.set([name, opt], this.cfg.main[opt])
344
+ } else {
345
+ // defaults
346
+ switch (opt) {
347
+ case 'sessionIdContext':
348
+ certsByHost.set([name, opt], 'haraka')
349
+ break
350
+ case 'isServer':
351
+ certsByHost.set([name, opt], true)
352
+ break
353
+ case 'key':
354
+ certsByHost.set([name, opt], 'tls_key.pem')
355
+ break
356
+ case 'cert':
357
+ certsByHost.set([name, opt], 'tls_cert.pem')
358
+ break
359
+ case 'dhparam':
360
+ certsByHost.set([name, opt], 'dhparams.pem')
361
+ break
362
+ case 'SNICallback':
363
+ certsByHost.set([name, opt], exports.SNICallback)
364
+ break
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ exports.load_default_opts = () => {
371
+ const cfg = certsByHost['*']
372
+
373
+ if (cfg.dhparam && typeof cfg.dhparam === 'string') {
374
+ log.debug(`loading dhparams from ${cfg.dhparam}`)
375
+ certsByHost.set('*.dhparam', this.config.get(cfg.dhparam, 'binary'))
376
+ }
377
+
378
+ if (cfg.ca && typeof cfg.ca === 'string') {
379
+ log.info(`loading CA certs from ${cfg.ca}`)
380
+ certsByHost.set('*.ca', this.config.get(cfg.ca, 'binary'))
381
+ }
382
+
383
+ // make non-array key/cert option into Arrays with one entry
384
+ if (!Array.isArray(cfg.key)) cfg.key = [cfg.key]
385
+ if (!Array.isArray(cfg.cert)) cfg.cert = [cfg.cert]
386
+
387
+ if (cfg.key.length !== cfg.cert.length) {
388
+ log.error(`number of keys (${cfg.key.length}) not equal to certs (${cfg.cert.length}).`)
389
+ }
390
+
391
+ // if key file has already been loaded, it'll be a Buffer.
392
+ if (typeof cfg.key[0] === 'string') {
393
+ // turn key/cert file names into actual key/cert binary data
394
+ const asArray = cfg.key.map((keyFileName) => {
395
+ if (!keyFileName) return
396
+ const key = this.config.get(keyFileName, 'binary')
397
+ if (!key) {
398
+ log.error(`tls key ${path.join(this.config.root_path, keyFileName)} could not be loaded.`)
399
+ }
400
+ return key
401
+ })
402
+ certsByHost.set('*.key', asArray)
403
+ }
404
+
405
+ if (typeof cfg.cert[0] === 'string') {
406
+ const asArray = cfg.cert.map((certFileName) => {
407
+ if (!certFileName) return
408
+ const cert = this.config.get(certFileName, 'binary')
409
+ if (!cert) {
410
+ log.error(`tls cert ${path.join(this.config.root_path, certFileName)} could not be loaded.`)
411
+ }
412
+ return cert
413
+ })
414
+ certsByHost.set('*.cert', asArray)
415
+ }
416
+
417
+ if (cfg.cert[0] && cfg.key[0]) {
418
+ this.tls_valid = true
419
+
420
+ // now that all opts are applied, generate TLS context
421
+ this.ensureDhparams(() => {
422
+ ctxByHost['*'] = tls.createSecureContext(cfg)
423
+ })
424
+ }
425
+ }
426
+
427
+ exports.SNICallback = function (servername, sniDone) {
428
+ log.debug(`SNI servername: ${servername}`)
429
+
430
+ sniDone(null, ctxByHost[servername] || ctxByHost['*'])
431
+ }
432
+
433
+ exports.get_certs_dir = async (tlsDir) => {
434
+ const r = {}
435
+ const watcher = async () => {
436
+ exports.get_certs_dir(tlsDir)
437
+ }
438
+ const dirOpts = { type: 'binary', watchCb: watcher }
439
+
440
+ const files = await this.config.getDir(tlsDir, dirOpts)
441
+ for (const file of files) {
442
+ try {
443
+ r[file.path] = await exports.parse_x509(file.data.toString())
444
+ } catch (err) {
445
+ log.debug(err.message)
446
+ }
447
+ }
448
+
449
+ log.debug(`found ${Object.keys(r).length} files in config/tls`)
450
+ if (Object.keys(r).length === 0) return
451
+
452
+ const s = {} // certs by name (CN)
453
+
454
+ for (const fp in r) {
455
+ if (r[fp].expire && r[fp].expire < new Date()) {
456
+ log.error(`${fp} expired on ${r[fp].expire}`)
457
+ }
458
+
459
+ // a file with a key and no cert, get name from file
460
+ if (!r[fp].names) r[fp].names = [path.parse(fp).name]
461
+
462
+ for (let name of r[fp].names) {
463
+ if (name[0] === '_') name = name.replace('_', '*') // windows
464
+ if (s[name] === undefined) s[name] = {}
465
+ if (!s[name].key && r[fp].keys) s[name].key = r[fp].keys[0]
466
+ if (!s[name].cert && r[fp].chain) {
467
+ s[name].cert = r[fp].chain[0]
468
+ s[name].file = fp
469
+ }
470
+ }
471
+ }
472
+
473
+ for (const cn in s) {
474
+ if (!s[cn].cert || !s[cn].key) {
475
+ delete s[cn]
476
+ continue
477
+ }
478
+
479
+ this.applySocketOpts(cn) // from tls.ini
480
+ certsByHost.set([cn, 'cert'], Buffer.from(s[cn].cert))
481
+ certsByHost.set([cn, 'key'], Buffer.from(s[cn].key))
482
+ certsByHost.set([cn, 'dhparam'], certsByHost['*'].dhparam, true)
483
+
484
+ // all opts are applied, generate TLS context
485
+ try {
486
+ ctxByHost[cn] = tls.createSecureContext(certsByHost.get([cn]))
487
+ } catch (err) {
488
+ log.error(`CN '${cn}' loading got: ${err.message}`)
489
+ delete ctxByHost[cn]
490
+ delete certsByHost[cn]
491
+ }
492
+ }
493
+
494
+ log.info(`found ${Object.keys(s).length} TLS certs in config/tls`)
495
+
496
+ return certsByHost // used only by tests
497
+ }
498
+
499
+ function openssl(crt, ...params) {
500
+ return new Promise((resolve) => {
501
+ let crtTxt = ''
502
+ let errTxt = ''
503
+
504
+ const o = spawn('openssl', params, { timeout: 2000 })
505
+ o.stdout.on('data', (data) => {
506
+ crtTxt += data
507
+ })
508
+
509
+ o.stderr.on('data', (data) => {
510
+ errTxt += data
511
+ })
512
+
513
+ o.on('close', (code) => {
514
+ if (code !== 0) {
515
+ log.error(`openssl ${params.join(' ')} failed with code ${code}: ${errTxt.trim()}`)
516
+ }
517
+ resolve(crtTxt)
518
+ })
519
+
520
+ o.stdin.write(crt)
521
+ o.stdin.write('\n')
522
+ })
523
+ }
524
+
525
+ exports.getSocketOpts = async (name) => {
526
+ // startup time, load the config/tls dir
527
+ if (!certsByHost['*']) this.load_tls_ini()
528
+
529
+ try {
530
+ await this.get_certs_dir('tls')
531
+ } catch (err) {
532
+ if (err.code !== 'ENOENT') {
533
+ log.error(err.message)
534
+ }
535
+ }
536
+
537
+ return certsByHost[name] || certsByHost['*']
538
+ }
539
+
540
+ function pipe(cleartext, socket) {
541
+ cleartext.socket = socket
542
+
543
+ function onError() {}
544
+
545
+ function onClose() {
546
+ socket.removeListener('error', onError)
547
+ socket.removeListener('close', onClose)
548
+ }
549
+
550
+ socket.on('error', onError)
551
+ socket.on('close', onClose)
552
+ }
553
+
554
+ exports.ensureDhparams = (done) => {
555
+ // empty/missing dhparams file
556
+ if (certsByHost['*'].dhparam) {
557
+ return done(null, certsByHost['*'].dhparam)
558
+ }
559
+
560
+ if (cluster.isWorker) return // only once, on the master process
561
+
562
+ const filePath = this.cfg.main.dhparam || 'dhparams.pem'
563
+ const fpResolved = path.resolve(exports.config.root_path, filePath)
564
+
565
+ log.info(`Generating a 2048 bit dhparams file at ${fpResolved}`)
566
+
567
+ const o = spawn('openssl', ['dhparam', '-out', fpResolved, '2048'], { timeout: 30000 })
568
+ o.stdout.on('data', (data) => {
569
+ // normally empty output
570
+ log.debug(data)
571
+ })
572
+
573
+ o.stderr.on('data', () => {
574
+ // this is the status gibberish `openssl dhparam` spews as it works
575
+ })
576
+
577
+ o.on('close', (code) => {
578
+ if (code !== 0) {
579
+ return done(`Error code: ${code}`)
580
+ }
581
+
582
+ log.info(`Saved to ${fpResolved}`)
583
+ const content = this.config.get(filePath, 'binary')
584
+
585
+ certsByHost.set('*.dhparam', content)
586
+ done(null, certsByHost['*'].dhparam)
587
+ })
588
+ }
589
+
590
+ exports.addOCSP = (server) => {
591
+ if (!ocsp) {
592
+ log.debug(`addOCSP: 'ocsp' not available`)
593
+ return
594
+ }
595
+
596
+ if (server.listenerCount('OCSPRequest') > 0) {
597
+ log.debug('OCSPRequest already listening')
598
+ return
599
+ }
600
+
601
+ log.debug('adding OCSPRequest listener')
602
+ server.on('OCSPRequest', (cert, issuer, ocr_cb) => {
603
+ log.debug(`OCSPRequest: ${cert}`)
604
+ ocsp.getOCSPURI(cert, async (err, uri) => {
605
+ log.debug(`OCSP Request, URI: ${uri}, err=${err}`)
606
+ if (err) return ocr_cb(err)
607
+ if (uri === null) return ocr_cb() // not working OCSP server
608
+
609
+ const req = ocsp.request.generate(cert, issuer)
610
+ const cached = await ocspCache.probe(req.id)
611
+
612
+ if (cached) {
613
+ log.debug(`OCSP cache: ${util.inspect(cached)}`)
614
+ return ocr_cb(null, cached.response)
615
+ }
616
+
617
+ const options = {
618
+ url: uri,
619
+ ocsp: req.data,
620
+ }
621
+
622
+ log.debug(`OCSP req:${util.inspect(req)}`)
623
+ ocspCache.request(req.id, options, ocr_cb)
624
+ })
625
+ })
626
+ }
627
+
628
+ exports.shutdown = () => {
629
+ if (ocsp) cleanOcspCache()
630
+ }
631
+
632
+ function cleanOcspCache() {
633
+ log.debug(`Cleaning ocspCache. How many keys? ${Object.keys(ocspCache.cache).length}`)
634
+ for (const key of Object.keys(ocspCache.cache)) {
635
+ clearTimeout(ocspCache.cache[key].timer)
636
+ }
637
+ }
638
+
639
+ exports.certsByHost = certsByHost
640
+ exports.ocsp = ocsp
641
+
642
+ exports.get_rejectUnauthorized = (rejectUnauthorized, port, port_list) => {
643
+ // console.log(`rejectUnauthorized: ${rejectUnauthorized}, port ${port}, list: ${port_list}`)
644
+
645
+ if (rejectUnauthorized) return true
646
+
647
+ return !!port_list.includes(port)
648
+ }
649
+
650
+ function createServer(cb) {
651
+ const server = net.createServer((cryptoSocket) => {
652
+ const socket = new pluggableStream(cryptoSocket)
653
+
654
+ exports.addOCSP(server)
655
+
656
+ socket.upgrade = (cb2) => {
657
+ log.debug('Upgrading to TLS')
658
+
659
+ socket.clean()
660
+
661
+ cryptoSocket.removeAllListeners('data')
662
+
663
+ const options = { ...certsByHost['*'] }
664
+ options.server = server // TLSSocket needs server for SNI to work
665
+
666
+ options.rejectUnauthorized = exports.get_rejectUnauthorized(
667
+ options.rejectUnauthorized,
668
+ cryptoSocket.localPort,
669
+ exports.cfg.main.requireAuthorized,
670
+ )
671
+
672
+ const cleartext = new tls.TLSSocket(cryptoSocket, options)
673
+
674
+ pipe(cleartext, cryptoSocket)
675
+
676
+ cleartext
677
+ .on('error', (exception) => {
678
+ exception.source = 'tls'
679
+ socket.emit('error', exception)
680
+ })
681
+ .on('secure', () => {
682
+ log.debug('TLS secured.')
683
+ socket.emit('secure')
684
+ const cipher = cleartext.getCipher()
685
+ cipher.version = cleartext.getProtocol()
686
+ if (cb2)
687
+ cb2(cleartext.authorized, cleartext.authorizationError, cleartext.getPeerCertificate(), cipher)
688
+ })
689
+
690
+ socket.cleartext = cleartext
691
+
692
+ if (socket._timeout) {
693
+ cleartext.setTimeout(socket._timeout)
694
+ }
695
+
696
+ cleartext.setKeepAlive(socket._keepalive)
697
+
698
+ socket.attach(socket.cleartext)
699
+ }
700
+
701
+ cb(socket)
702
+ })
703
+
704
+ return server
705
+ }
706
+
707
+ function getCertFor(host) {
708
+ if (host && certsByHost[host]) return certsByHost[host]
709
+ return certsByHost['*'] // the default TLS cert
710
+ }
711
+
712
+ function connect(conn_options = {}) {
713
+ // called by outbound/client_pool, smtp_client, plugins/spamassassin,avg,clamd,
714
+ // plugins/auth/auth_proxy
715
+
716
+ const cryptoSocket = net.connect(conn_options)
717
+ const socket = new pluggableStream(cryptoSocket)
718
+
719
+ socket.upgrade = (options, cb2) => {
720
+ socket.clean()
721
+ cryptoSocket.removeAllListeners('data')
722
+
723
+ if (exports.tls_valid) {
724
+ const host = conn_options.host
725
+ if (exports.cfg === undefined) exports.load_tls_ini()
726
+ if (exports.cfg.mutual_auth_hosts[host]) {
727
+ options = { ...options, ...getCertFor(exports.cfg.mutual_auth_hosts[host]) }
728
+ } else if (exports.cfg.mutual_auth_hosts_exclude[host]) {
729
+ // send no client cert
730
+ } else if (exports.cfg.main.mutual_tls) {
731
+ options = { ...options, ...getCertFor(host) }
732
+ }
733
+ }
734
+ options.socket = cryptoSocket
735
+
736
+ const cleartext = tls.connect(options)
737
+
738
+ pipe(cleartext, cryptoSocket)
739
+
740
+ cleartext.on('error', (err) => {
741
+ err.source = 'tls'
742
+ socket.emit('error', err)
743
+ })
744
+
745
+ cleartext.once('secureConnect', () => {
746
+ log.debug('client TLS secured.')
747
+ const cipher = cleartext.getCipher()
748
+ cipher.version = cleartext.getProtocol()
749
+ if (cb2) cb2(cleartext.authorized, cleartext.authorizationError, cleartext.getPeerCertificate(), cipher)
750
+ })
751
+
752
+ socket.cleartext = cleartext
753
+
754
+ if (socket._timeout) {
755
+ cleartext.setTimeout(socket._timeout)
756
+ }
757
+
758
+ cleartext.setKeepAlive(socket._keepalive)
759
+
760
+ socket.attach(socket.cleartext)
761
+
762
+ log.debug('client TLS upgrade in progress, awaiting secured.')
763
+ }
764
+
765
+ return socket
766
+ }
767
+
768
+ exports.connect = connect
769
+ exports.createConnection = connect
770
+ exports.Server = createServer
771
+ exports.createServer = createServer