imapflow 1.7.8 → 2.0.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 (296) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +8 -2
  3. package/dist/cjs/charsets.d.ts +1 -0
  4. package/dist/cjs/charsets.js +294 -0
  5. package/dist/cjs/commands/append.d.ts +22 -0
  6. package/dist/cjs/commands/append.js +151 -0
  7. package/dist/cjs/commands/authenticate.d.ts +24 -0
  8. package/dist/cjs/commands/authenticate.js +223 -0
  9. package/dist/cjs/commands/capability.d.ts +8 -0
  10. package/dist/cjs/commands/capability.js +32 -0
  11. package/dist/cjs/commands/close.d.ts +8 -0
  12. package/dist/cjs/commands/close.js +39 -0
  13. package/dist/cjs/commands/compress.d.ts +8 -0
  14. package/dist/cjs/commands/compress.js +56 -0
  15. package/dist/cjs/commands/copy.d.ts +13 -0
  16. package/dist/cjs/commands/copy.js +44 -0
  17. package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
  18. package/dist/cjs/commands/copyuid-parser.js +32 -0
  19. package/dist/cjs/commands/create.d.ts +11 -0
  20. package/dist/cjs/commands/create.js +80 -0
  21. package/dist/cjs/commands/delete.d.ts +11 -0
  22. package/dist/cjs/commands/delete.js +40 -0
  23. package/dist/cjs/commands/enable.d.ts +9 -0
  24. package/dist/cjs/commands/enable.js +61 -0
  25. package/dist/cjs/commands/esearch-parser.d.ts +17 -0
  26. package/dist/cjs/commands/esearch-parser.js +91 -0
  27. package/dist/cjs/commands/expunge.d.ts +12 -0
  28. package/dist/cjs/commands/expunge.js +60 -0
  29. package/dist/cjs/commands/fetch.d.ts +30 -0
  30. package/dist/cjs/commands/fetch.js +241 -0
  31. package/dist/cjs/commands/id.d.ts +10 -0
  32. package/dist/cjs/commands/id.js +80 -0
  33. package/dist/cjs/commands/idle.d.ts +9 -0
  34. package/dist/cjs/commands/idle.js +347 -0
  35. package/dist/cjs/commands/list.d.ts +16 -0
  36. package/dist/cjs/commands/list.js +518 -0
  37. package/dist/cjs/commands/login.d.ts +11 -0
  38. package/dist/cjs/commands/login.js +42 -0
  39. package/dist/cjs/commands/logout.d.ts +8 -0
  40. package/dist/cjs/commands/logout.js +47 -0
  41. package/dist/cjs/commands/move.d.ts +13 -0
  42. package/dist/cjs/commands/move.js +57 -0
  43. package/dist/cjs/commands/namespace.d.ts +25 -0
  44. package/dist/cjs/commands/namespace.js +139 -0
  45. package/dist/cjs/commands/noop.d.ts +8 -0
  46. package/dist/cjs/commands/noop.js +22 -0
  47. package/dist/cjs/commands/quota.d.ts +10 -0
  48. package/dist/cjs/commands/quota.js +119 -0
  49. package/dist/cjs/commands/rename.d.ts +12 -0
  50. package/dist/cjs/commands/rename.js +48 -0
  51. package/dist/cjs/commands/search.d.ts +15 -0
  52. package/dist/cjs/commands/search.js +228 -0
  53. package/dist/cjs/commands/select.d.ts +25 -0
  54. package/dist/cjs/commands/select.js +250 -0
  55. package/dist/cjs/commands/starttls.d.ts +8 -0
  56. package/dist/cjs/commands/starttls.js +30 -0
  57. package/dist/cjs/commands/status-fields.d.ts +14 -0
  58. package/dist/cjs/commands/status-fields.js +61 -0
  59. package/dist/cjs/commands/status.d.ts +12 -0
  60. package/dist/cjs/commands/status.js +108 -0
  61. package/dist/cjs/commands/store.d.ts +19 -0
  62. package/dist/cjs/commands/store.js +93 -0
  63. package/dist/cjs/commands/subscribe.d.ts +9 -0
  64. package/dist/cjs/commands/subscribe.js +31 -0
  65. package/dist/cjs/commands/unsubscribe.d.ts +9 -0
  66. package/dist/cjs/commands/unsubscribe.js +31 -0
  67. package/dist/cjs/connection-deadline.d.ts +49 -0
  68. package/dist/cjs/connection-deadline.js +91 -0
  69. package/dist/cjs/errors.d.ts +83 -0
  70. package/dist/cjs/errors.js +13 -0
  71. package/dist/cjs/handler/imap-compiler.d.ts +24 -0
  72. package/dist/cjs/handler/imap-compiler.js +285 -0
  73. package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
  74. package/dist/cjs/handler/imap-formal-syntax.js +121 -0
  75. package/dist/cjs/handler/imap-handler.d.ts +9 -0
  76. package/dist/cjs/handler/imap-handler.js +10 -0
  77. package/dist/cjs/handler/imap-parser.d.ts +16 -0
  78. package/dist/cjs/handler/imap-parser.js +90 -0
  79. package/dist/cjs/handler/imap-stream.d.ts +181 -0
  80. package/dist/cjs/handler/imap-stream.js +446 -0
  81. package/dist/cjs/handler/limits.d.ts +25 -0
  82. package/dist/cjs/handler/limits.js +51 -0
  83. package/dist/cjs/handler/parser-instance.d.ts +68 -0
  84. package/dist/cjs/handler/parser-instance.js +223 -0
  85. package/dist/cjs/handler/token-parser.d.ts +91 -0
  86. package/dist/cjs/handler/token-parser.js +673 -0
  87. package/dist/cjs/handler/types.d.ts +91 -0
  88. package/dist/cjs/handler/types.js +4 -0
  89. package/dist/cjs/imap-commands.d.ts +16 -0
  90. package/dist/cjs/imap-commands.js +74 -0
  91. package/dist/cjs/imap-flow.d.ts +676 -0
  92. package/dist/cjs/imap-flow.js +3949 -0
  93. package/dist/cjs/jp-decoder.d.ts +12 -0
  94. package/dist/cjs/jp-decoder.js +79 -0
  95. package/dist/cjs/limited-passthrough.d.ts +25 -0
  96. package/dist/cjs/limited-passthrough.js +54 -0
  97. package/dist/cjs/logger.d.ts +3 -0
  98. package/dist/cjs/logger.js +11 -0
  99. package/dist/cjs/package-info.d.ts +3 -0
  100. package/dist/cjs/package-info.js +7 -0
  101. package/dist/cjs/package.json +3 -0
  102. package/dist/cjs/proxy-connection.d.ts +33 -0
  103. package/dist/cjs/proxy-connection.js +392 -0
  104. package/dist/cjs/search-compiler.d.ts +34 -0
  105. package/dist/cjs/search-compiler.js +476 -0
  106. package/dist/cjs/special-use.d.ts +22 -0
  107. package/dist/cjs/special-use.js +911 -0
  108. package/dist/cjs/tools.d.ts +427 -0
  109. package/dist/cjs/tools.js +1496 -0
  110. package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
  111. package/dist/cjs/types.js +5 -0
  112. package/dist/esm/charsets.d.ts +1 -0
  113. package/{lib → dist/esm}/charsets.js +1 -6
  114. package/dist/esm/commands/append.d.ts +22 -0
  115. package/{lib → dist/esm}/commands/append.js +22 -52
  116. package/dist/esm/commands/authenticate.d.ts +24 -0
  117. package/{lib → dist/esm}/commands/authenticate.js +62 -87
  118. package/dist/esm/commands/capability.d.ts +8 -0
  119. package/{lib → dist/esm}/commands/capability.js +6 -9
  120. package/dist/esm/commands/close.d.ts +8 -0
  121. package/{lib → dist/esm}/commands/close.js +6 -10
  122. package/dist/esm/commands/compress.d.ts +8 -0
  123. package/{lib → dist/esm}/commands/compress.js +7 -11
  124. package/dist/esm/commands/copy.d.ts +13 -0
  125. package/{lib → dist/esm}/commands/copy.js +12 -20
  126. package/dist/esm/commands/copyuid-parser.d.ts +11 -0
  127. package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
  128. package/dist/esm/commands/create.d.ts +11 -0
  129. package/{lib → dist/esm}/commands/create.js +13 -27
  130. package/dist/esm/commands/delete.d.ts +11 -0
  131. package/{lib → dist/esm}/commands/delete.js +9 -14
  132. package/dist/esm/commands/enable.d.ts +9 -0
  133. package/{lib → dist/esm}/commands/enable.js +23 -30
  134. package/dist/esm/commands/esearch-parser.d.ts +17 -0
  135. package/dist/esm/commands/esearch-parser.js +88 -0
  136. package/dist/esm/commands/expunge.d.ts +12 -0
  137. package/{lib → dist/esm}/commands/expunge.js +17 -22
  138. package/dist/esm/commands/fetch.d.ts +30 -0
  139. package/{lib → dist/esm}/commands/fetch.js +32 -64
  140. package/dist/esm/commands/id.d.ts +10 -0
  141. package/{lib → dist/esm}/commands/id.js +17 -23
  142. package/dist/esm/commands/idle.d.ts +9 -0
  143. package/{lib → dist/esm}/commands/idle.js +47 -81
  144. package/dist/esm/commands/list.d.ts +16 -0
  145. package/{lib → dist/esm}/commands/list.js +56 -121
  146. package/dist/esm/commands/login.d.ts +11 -0
  147. package/{lib → dist/esm}/commands/login.js +10 -15
  148. package/dist/esm/commands/logout.d.ts +8 -0
  149. package/{lib → dist/esm}/commands/logout.js +9 -11
  150. package/dist/esm/commands/move.d.ts +13 -0
  151. package/{lib → dist/esm}/commands/move.js +13 -21
  152. package/dist/esm/commands/namespace.d.ts +25 -0
  153. package/{lib → dist/esm}/commands/namespace.js +34 -44
  154. package/dist/esm/commands/noop.d.ts +8 -0
  155. package/{lib → dist/esm}/commands/noop.js +6 -7
  156. package/dist/esm/commands/quota.d.ts +10 -0
  157. package/{lib → dist/esm}/commands/quota.js +18 -36
  158. package/dist/esm/commands/rename.d.ts +12 -0
  159. package/{lib → dist/esm}/commands/rename.js +10 -15
  160. package/dist/esm/commands/search.d.ts +15 -0
  161. package/{lib → dist/esm}/commands/search.js +36 -135
  162. package/dist/esm/commands/select.d.ts +25 -0
  163. package/{lib → dist/esm}/commands/select.js +33 -64
  164. package/dist/esm/commands/starttls.d.ts +8 -0
  165. package/{lib → dist/esm}/commands/starttls.js +6 -8
  166. package/dist/esm/commands/status-fields.d.ts +14 -0
  167. package/{lib → dist/esm}/commands/status-fields.js +5 -16
  168. package/dist/esm/commands/status.d.ts +12 -0
  169. package/{lib → dist/esm}/commands/status.js +18 -29
  170. package/dist/esm/commands/store.d.ts +19 -0
  171. package/{lib → dist/esm}/commands/store.js +24 -37
  172. package/dist/esm/commands/subscribe.d.ts +9 -0
  173. package/{lib → dist/esm}/commands/subscribe.js +8 -12
  174. package/dist/esm/commands/unsubscribe.d.ts +9 -0
  175. package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
  176. package/dist/esm/connection-deadline.d.ts +49 -0
  177. package/{lib → dist/esm}/connection-deadline.js +14 -25
  178. package/dist/esm/errors.d.ts +83 -0
  179. package/dist/esm/errors.js +9 -0
  180. package/dist/esm/handler/imap-compiler.d.ts +24 -0
  181. package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
  182. package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
  183. package/dist/esm/handler/imap-formal-syntax.js +117 -0
  184. package/dist/esm/handler/imap-handler.d.ts +9 -0
  185. package/dist/esm/handler/imap-handler.js +9 -0
  186. package/dist/esm/handler/imap-parser.d.ts +16 -0
  187. package/{lib → dist/esm}/handler/imap-parser.js +31 -44
  188. package/dist/esm/handler/imap-stream.d.ts +181 -0
  189. package/{lib → dist/esm}/handler/imap-stream.js +29 -121
  190. package/dist/esm/handler/limits.d.ts +25 -0
  191. package/{lib → dist/esm}/handler/limits.js +13 -22
  192. package/dist/esm/handler/parser-instance.d.ts +68 -0
  193. package/{lib → dist/esm}/handler/parser-instance.js +19 -47
  194. package/dist/esm/handler/token-parser.d.ts +91 -0
  195. package/{lib → dist/esm}/handler/token-parser.js +71 -155
  196. package/dist/esm/handler/types.d.ts +91 -0
  197. package/dist/esm/handler/types.js +3 -0
  198. package/dist/esm/imap-commands.d.ts +16 -0
  199. package/dist/esm/imap-commands.js +67 -0
  200. package/dist/esm/imap-flow.d.ts +676 -0
  201. package/{lib → dist/esm}/imap-flow.js +761 -1789
  202. package/dist/esm/jp-decoder.d.ts +12 -0
  203. package/{lib → dist/esm}/jp-decoder.js +6 -21
  204. package/dist/esm/limited-passthrough.d.ts +25 -0
  205. package/{lib → dist/esm}/limited-passthrough.js +7 -20
  206. package/dist/esm/logger.d.ts +3 -0
  207. package/dist/esm/logger.js +4 -0
  208. package/dist/esm/package-info.d.ts +3 -0
  209. package/dist/esm/package-info.js +4 -0
  210. package/dist/esm/package.json +3 -0
  211. package/dist/esm/proxy-connection.d.ts +33 -0
  212. package/{lib → dist/esm}/proxy-connection.js +56 -127
  213. package/dist/esm/search-compiler.d.ts +34 -0
  214. package/{lib → dist/esm}/search-compiler.js +54 -110
  215. package/dist/esm/special-use.d.ts +22 -0
  216. package/dist/esm/special-use.js +907 -0
  217. package/dist/esm/tools.d.ts +427 -0
  218. package/dist/esm/tools.js +1446 -0
  219. package/dist/esm/types.d.ts +828 -0
  220. package/dist/esm/types.js +4 -0
  221. package/package.json +60 -20
  222. package/.gitattributes +0 -1
  223. package/.github/CODE_OF_CONDUCT.md +0 -76
  224. package/.github/FUNDING.yml +0 -4
  225. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  226. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  227. package/.github/contributing.md +0 -17
  228. package/.github/workflows/release.yaml +0 -36
  229. package/.github/workflows/stale.yml +0 -29
  230. package/.github/workflows/test.yml +0 -51
  231. package/.ncurc.js +0 -4
  232. package/.prettierignore +0 -4
  233. package/.prettierrc.js +0 -8
  234. package/.release-please-manifest.json +0 -3
  235. package/CLAUDE.md +0 -104
  236. package/Gruntfile.js +0 -23
  237. package/eslint.config.js +0 -45
  238. package/lib/handler/imap-formal-syntax.js +0 -189
  239. package/lib/handler/imap-handler.js +0 -17
  240. package/lib/imap-commands.js +0 -45
  241. package/lib/logger.js +0 -5
  242. package/lib/special-use.js +0 -923
  243. package/lib/tools.js +0 -1612
  244. package/release-please-config.json +0 -10
  245. package/test/authentication-test.js +0 -101
  246. package/test/auto-idle-test.js +0 -470
  247. package/test/bodystructure-test.js +0 -899
  248. package/test/charsets-test.js +0 -161
  249. package/test/commands-branches-test.js +0 -1095
  250. package/test/commands-integration-test.js +0 -11124
  251. package/test/commands-test.js +0 -73
  252. package/test/connection-edge-cases-test.js +0 -1828
  253. package/test/connection-test.js +0 -162
  254. package/test/copyuid-parser-test.js +0 -173
  255. package/test/fetch-generator-test.js +0 -218
  256. package/test/fixtures/fake-timers.js +0 -115
  257. package/test/fixtures/serialized-mimetorture.js +0 -2738
  258. package/test/fixtures/test-client.js +0 -101
  259. package/test/fixtures/test-tls.js +0 -8
  260. package/test/handler-branches-test.js +0 -310
  261. package/test/idle-polling-test.js +0 -518
  262. package/test/imap-compiler-test.js +0 -809
  263. package/test/imap-flow-compress-test.js +0 -166
  264. package/test/imap-flow-coverage-test.js +0 -612
  265. package/test/imap-flow-fetch-download-test.js +0 -909
  266. package/test/imap-flow-internals-test.js +0 -725
  267. package/test/imap-flow-methods-test.js +0 -889
  268. package/test/imap-flow-proxy-paths-test.js +0 -366
  269. package/test/imap-flow-secure-test.js +0 -573
  270. package/test/imap-flow-server-test.js +0 -1474
  271. package/test/imap-formal-syntax-test.js +0 -293
  272. package/test/imap-parser-test.js +0 -1474
  273. package/test/imap-stream-edge-cases-test.js +0 -666
  274. package/test/imap-stream-test.js +0 -177
  275. package/test/imapflow-test.js +0 -258
  276. package/test/integration/README.md +0 -52
  277. package/test/integration/dovecot-test.conf +0 -27
  278. package/test/integration/rev2-live-test.js +0 -431
  279. package/test/integration/run-rev2-tests.sh +0 -75
  280. package/test/integration-test.js +0 -83
  281. package/test/jp-decoder-test.js +0 -304
  282. package/test/limited-passthrough-test.js +0 -299
  283. package/test/memory-cleanup-test.js +0 -144
  284. package/test/memory-leak-test.js +0 -667
  285. package/test/parser-limits-test.js +0 -292
  286. package/test/proxy-connection-test.js +0 -738
  287. package/test/reliability-improvements-test.js +0 -548
  288. package/test/search-compiler-test.js +0 -1300
  289. package/test/search-test.js +0 -329
  290. package/test/special-use-test.js +0 -418
  291. package/test/starttls-injection-test.js +0 -181
  292. package/test/tag-correlation-test.js +0 -333
  293. package/test/timer-policy-test.js +0 -227
  294. package/test/token-parser-test.js +0 -456
  295. package/test/tools-test.js +0 -2013
  296. package/test/unhandled-rejection-test.js +0 -661
@@ -1,738 +0,0 @@
1
- 'use strict';
2
-
3
- // Proxy connection tests.
4
- //
5
- // ImapFlow owns HTTP/HTTPS CONNECT negotiation and applies one connection-wide deadline to DNS,
6
- // proxy negotiation and the transport handshake. The DNS policy differs per proxy protocol, so
7
- // these tests pin what each mode is handed:
8
- // * HTTP/HTTPS - the destination hostname reaches the proxy unresolved
9
- // * SOCKS4 - destination hostnames are resolved locally to IPv4
10
- // * SOCKS4a - destination hostnames are preserved for remote DNS
11
- // * SOCKS5 - destination hostnames are preserved, IP literals pass through
12
- // and that ImapFlow never resolves a proxy endpoint itself.
13
-
14
- const net = require('net');
15
- const { EventEmitter } = require('events');
16
- const proxyquire = require('proxyquire');
17
- const { ConnectionDeadline } = require('../lib/connection-deadline');
18
-
19
- // Socket stand-in for the stubbed net/tls connect calls. Tests drive it by emitting 'data'.
20
- const createFakeSocket = () => {
21
- const socket = new EventEmitter();
22
- socket.writes = [];
23
- socket.unshifted = [];
24
- socket.destroyed = false;
25
- socket.write = chunk => {
26
- socket.writes.push(chunk.toString('binary'));
27
- return true;
28
- };
29
- socket.unshift = chunk => socket.unshifted.push(chunk.toString('binary'));
30
- socket.destroy = () => {
31
- socket.destroyed = true;
32
- };
33
- socket.setTimeout = () => {};
34
- socket.end = () => {};
35
- socket.paused = false;
36
- socket.pause = () => {
37
- socket.paused = true;
38
- return socket;
39
- };
40
- socket.resume = () => {
41
- socket.paused = false;
42
- return socket;
43
- };
44
- return socket;
45
- };
46
-
47
- // Builds a connect() stub that records its options and hands out fake sockets.
48
- const createConnectStub =
49
- ({ calls, sockets, autoConnect = true }) =>
50
- (options, onConnected) => {
51
- calls.push(options);
52
- const socket = createFakeSocket();
53
- sockets.push(socket);
54
- if (autoConnect) {
55
- setImmediate(() => onConnected());
56
- }
57
- return socket;
58
- };
59
-
60
- // dns stub that fails loudly: no proxy mode may resolve an endpoint through ImapFlow itself.
61
- const createDnsStub = (resolve4Result, onCall) => ({
62
- promises: {
63
- resolve4: async hostname => {
64
- if (onCall) {
65
- onCall('resolve4', hostname);
66
- }
67
- return resolve4Result || [];
68
- },
69
- resolve: async hostname => {
70
- if (onCall) {
71
- onCall('resolve', hostname);
72
- }
73
- return resolve4Result || [];
74
- }
75
- }
76
- });
77
-
78
- const createMockLogger = () => {
79
- const logs = { info: [], error: [] };
80
- return {
81
- info: msg => logs.info.push(msg),
82
- error: msg => logs.error.push(msg),
83
- _logs: logs
84
- };
85
- };
86
-
87
- const tick = () => new Promise(resolve => setImmediate(resolve));
88
-
89
- // Loads proxy-connection with stubbed transports. `net` keeps its real address helpers so
90
- // isIP/isIPv6 behave normally.
91
- const loadProxyModule = ({ netConnect, tlsConnect, socksCreateConnection, dnsStub } = {}) =>
92
- proxyquire('../lib/proxy-connection', {
93
- net: Object.assign({}, net, netConnect ? { connect: netConnect } : {}),
94
- tls: tlsConnect ? { connect: tlsConnect } : { connect: () => createFakeSocket() },
95
- socks: { SocksClient: { createConnection: socksCreateConnection || (async () => ({ socket: createFakeSocket() })) } },
96
- dns: dnsStub || createDnsStub()
97
- });
98
-
99
- const respondOk = socket => socket.emit('data', Buffer.from('HTTP/1.1 200 Connection established\r\n\r\n'));
100
-
101
- // Loads the module with a stubbed transport and returns everything an HTTP CONNECT test needs:
102
- // the recorded connect options, the fake sockets handed out, and a capturing logger. Mirrors
103
- // socksCase() further down.
104
- const httpCase = ({ secureProxy = false, autoConnect = true, dnsStub } = {}) => {
105
- const calls = [];
106
- const sockets = [];
107
- const connectStub = createConnectStub({ calls, sockets, autoConnect });
108
- const { proxyConnection, detachEarlyErrorHandler } = loadProxyModule({
109
- [secureProxy ? 'tlsConnect' : 'netConnect']: connectStub,
110
- dnsStub
111
- });
112
- return { proxyConnection, detachEarlyErrorHandler, calls, sockets, logger: createMockLogger() };
113
- };
114
-
115
- // ============================================
116
- // HTTP / HTTPS CONNECT
117
- // ============================================
118
-
119
- module.exports['Proxy Connection: HTTP CONNECT preserves the destination hostname'] = async test => {
120
- let dnsCalls = [];
121
- const { proxyConnection, calls, sockets, logger } = httpCase({
122
- dnsStub: createDnsStub([], (method, hostname) => dnsCalls.push([method, hostname]))
123
- });
124
-
125
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
126
- await tick();
127
-
128
- test.deepEqual(calls[0], { host: 'proxy.example.com', port: 8080 }, 'the proxy endpoint hostname is passed unresolved');
129
- test.ok(sockets[0].writes[0].startsWith('CONNECT mail.example.com:993 HTTP/1.1\r\n'), 'the request line carries the unresolved destination');
130
- test.ok(sockets[0].writes[0].includes('Host: mail.example.com:993\r\n'), 'the Host header carries the unresolved destination');
131
- test.deepEqual(dnsCalls, [], 'ImapFlow performs no DNS lookup of its own for an HTTP proxy');
132
-
133
- respondOk(sockets[0]);
134
- const socket = await promise;
135
-
136
- test.equal(socket, sockets[0], 'the tunnelled socket is returned');
137
- test.ok(socket.listenerCount('error') >= 1, 'the returned socket carries an early error listener');
138
- test.equal(logger._logs.info.length, 1);
139
- test.ok(logger._logs.info[0].msg.includes('HTTP proxy'));
140
- test.done();
141
- };
142
-
143
- module.exports['Proxy Connection: HTTP CONNECT brackets an IPv6 destination'] = async test => {
144
- const { proxyConnection, sockets, logger } = httpCase();
145
-
146
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', '2001:db8::5', 993);
147
- await tick();
148
-
149
- const request = sockets[0].writes[0];
150
- test.ok(request.startsWith('CONNECT [2001:db8::5]:993 HTTP/1.1\r\n'), 'the request line uses a bracketed authority');
151
- test.ok(request.includes('Host: [2001:db8::5]:993\r\n'), 'the Host header uses a bracketed authority');
152
-
153
- respondOk(sockets[0]);
154
- await promise;
155
- test.done();
156
- };
157
-
158
- module.exports['Proxy Connection: HTTP CONNECT does not double-bracket an IPv6 destination'] = async test => {
159
- const { proxyConnection, sockets, logger } = httpCase();
160
-
161
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', '[2001:db8::5]', 993);
162
- await tick();
163
-
164
- test.ok(sockets[0].writes[0].startsWith('CONNECT [2001:db8::5]:993 HTTP/1.1\r\n'), 'an already bracketed literal stays single-bracketed');
165
-
166
- respondOk(sockets[0]);
167
- await promise;
168
- test.done();
169
- };
170
-
171
- module.exports['Proxy Connection: HTTP proxy endpoint given as an IPv6 URL uses a bare literal'] = async test => {
172
- const { proxyConnection, calls, sockets, logger } = httpCase();
173
-
174
- const promise = proxyConnection(logger, 'http://[2001:db8::1]:8080', 'mail.example.com', 993);
175
- await tick();
176
-
177
- test.equal(calls[0].host, '2001:db8::1', 'socket options get the bare literal, not the bracketed URL form');
178
-
179
- respondOk(sockets[0]);
180
- await promise;
181
-
182
- test.ok(logger._logs.info[0].proxyUrl.includes('[2001:db8::1]'), 'logs keep a valid bracketed URL');
183
- test.done();
184
- };
185
-
186
- module.exports['Proxy Connection: HTTPS proxy sets SNI for a hostname endpoint only'] = async test => {
187
- let { proxyConnection, calls: hostnameCalls, sockets: hostnameSockets, logger } = httpCase({ secureProxy: true });
188
-
189
- let promise = proxyConnection(logger, 'https://proxy.example.com:8443', 'mail.example.com', 993);
190
- await tick();
191
-
192
- test.equal(hostnameCalls[0].servername, 'proxy.example.com', 'SNI and hostname verification target the proxy, not the IMAP destination');
193
- respondOk(hostnameSockets[0]);
194
- await promise;
195
-
196
- let { proxyConnection: connectViaLiteral, calls: literalCalls, sockets: literalSockets } = httpCase({ secureProxy: true });
197
- promise = connectViaLiteral(logger, 'https://10.0.0.1:8443', 'mail.example.com', 993);
198
- await tick();
199
-
200
- test.equal(literalCalls[0].servername, undefined, 'an IP-literal endpoint gets no servername');
201
- test.equal(literalCalls[0].host, '10.0.0.1');
202
- respondOk(literalSockets[0]);
203
- await promise;
204
-
205
- test.done();
206
- };
207
-
208
- module.exports['Proxy Connection: HTTP CONNECT sends Basic credentials and keeps them out of logs'] = async test => {
209
- const { proxyConnection, sockets, logger } = httpCase();
210
-
211
- const promise = proxyConnection(logger, 'http://user:secret123@proxy.example.com:8080', 'mail.example.com', 993);
212
- await tick();
213
-
214
- const request = sockets[0].writes[0];
215
- const authHeader = request.match(/Proxy-Authorization: Basic (\S+)/);
216
- test.ok(authHeader, 'credentials are sent in the Proxy-Authorization header');
217
- test.equal(Buffer.from(authHeader[1], 'base64').toString(), 'user:secret123');
218
- test.ok(!request.includes('secret123'), 'credentials appear only base64 encoded, never in clear text');
219
-
220
- respondOk(sockets[0]);
221
- await promise;
222
-
223
- test.ok(!logger._logs.info[0].proxyUrl.includes('secret123'), 'the success log is redacted');
224
- test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
225
- test.done();
226
- };
227
-
228
- module.exports['Proxy Connection: HTTP CONNECT rejects a non-2xx response and redacts the log'] = async test => {
229
- const { proxyConnection, sockets, logger } = httpCase();
230
-
231
- const promise = proxyConnection(logger, 'http://user:secret123@proxy.example.com:8080', 'mail.example.com', 993);
232
- await tick();
233
- sockets[0].emit('data', Buffer.from('HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'));
234
-
235
- let err = await promise.then(() => null).catch(e => e);
236
- test.ok(err, 'a refused CONNECT rejects');
237
- test.equal(err.code, 'EPROXY');
238
- test.ok(/407/.test(err.message), 'the status code is reported');
239
- test.ok(sockets[0].destroyed, 'the socket is destroyed');
240
- test.ok(!logger._logs.error[0].proxyUrl.includes('secret123'), 'the failure log is redacted');
241
- test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
242
- test.done();
243
- };
244
-
245
- module.exports['Proxy Connection: HTTP CONNECT bounds the response header buffer'] = async test => {
246
- const { proxyConnection, sockets, logger } = httpCase();
247
-
248
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
249
- await tick();
250
-
251
- // A proxy that never sends the header terminator must not grow memory without bound
252
- for (let i = 0; i < 10 && !sockets[0].destroyed; i++) {
253
- sockets[0].emit('data', Buffer.alloc(16 * 1024, 0x41));
254
- }
255
-
256
- let err = await promise.then(() => null).catch(e => e);
257
- test.ok(err, 'the oversized header response rejects');
258
- test.equal(err.code, 'EPROXY');
259
- test.ok(/headers too large/i.test(err.message));
260
- test.ok(sockets[0].destroyed, 'the socket is destroyed');
261
- test.done();
262
- };
263
-
264
- module.exports['Proxy Connection: HTTP CONNECT preserves bytes after the header terminator'] = async test => {
265
- const { proxyConnection, sockets, logger } = httpCase();
266
-
267
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
268
- await tick();
269
-
270
- // The destination greeting arrives in the same segment as the CONNECT response
271
- sockets[0].emit('data', Buffer.from('HTTP/1.1 200 OK\r\n\r\n* OK [CAPABILITY IMAP4rev1] ready\r\n'));
272
-
273
- const socket = await promise;
274
- test.deepEqual(socket.unshifted, ['* OK [CAPABILITY IMAP4rev1] ready\r\n'], 'trailing bytes are pushed back for the next consumer');
275
- test.done();
276
- };
277
-
278
- module.exports['Proxy Connection: HTTP CONNECT rejects a destination with CRLF'] = async test => {
279
- const { proxyConnection, calls, logger } = httpCase();
280
-
281
- let err = await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com\r\nX-Injected: 1', 993)
282
- .then(() => null)
283
- .catch(e => e);
284
-
285
- test.ok(err, 'header injection through the destination is rejected');
286
- test.equal(err.code, 'EPROXY');
287
- test.deepEqual(calls, [], 'no socket is opened for an invalid destination');
288
- test.done();
289
- };
290
-
291
- module.exports['Proxy Connection: HTTP CONNECT rejects an invalid destination port'] = async test => {
292
- const { proxyConnection, logger } = httpCase();
293
-
294
- let err = await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 0)
295
- .then(() => null)
296
- .catch(e => e);
297
-
298
- test.ok(err, 'a missing destination port is rejected');
299
- test.equal(err.code, 'EPROXY');
300
- test.done();
301
- };
302
-
303
- module.exports['Proxy Connection: an early socket close rejects the tunnel'] = async test => {
304
- const { proxyConnection, sockets, logger } = httpCase();
305
-
306
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
307
- await tick();
308
- sockets[0].emit('close');
309
-
310
- let err = await promise.then(() => null).catch(e => e);
311
- test.ok(err, 'a proxy that hangs up before responding rejects');
312
- test.equal(err.code, 'EPROXY');
313
- test.done();
314
- };
315
-
316
- module.exports['Proxy Connection: a late socket event cannot settle the tunnel twice'] = async test => {
317
- const { proxyConnection, sockets, logger } = httpCase();
318
-
319
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
320
- await tick();
321
- respondOk(sockets[0]);
322
- const socket = await promise;
323
-
324
- // Late events after settlement: the temporary listeners are gone, so nothing re-settles and
325
- // nothing throws. Only the early error guard remains.
326
- test.equal(socket.listenerCount('close'), 0, 'the temporary close listener was removed');
327
- test.doesNotThrow(() => socket.emit('error', new Error('late error')), 'a late error is absorbed by the early error guard');
328
- test.doesNotThrow(() => socket.emit('data', Buffer.from('HTTP/1.1 500 late\r\n\r\n')), 'late data is no longer parsed as a CONNECT response');
329
- test.equal(logger._logs.error.length, 1, 'the late error was logged once by the early guard, not reported as a proxy failure');
330
- test.done();
331
- };
332
-
333
- // ============================================
334
- // Connection deadline
335
- // ============================================
336
-
337
- module.exports['Proxy Connection: HTTP CONNECT timeout destroys the in-flight socket'] = async test => {
338
- const { proxyConnection, sockets, logger } = httpCase();
339
-
340
- // The proxy accepts the connection and never answers the CONNECT request
341
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993, { connectionTimeout: 60 });
342
-
343
- let err = await promise.then(() => null).catch(e => e);
344
- test.ok(err, 'the stalled CONNECT rejects');
345
- test.equal(err.code, 'CONNECT_TIMEOUT', 'a proxy phase expiry uses the shared connection timeout code');
346
- test.equal(err.details.connectionTimeout, 60, 'the configured timeout is reported');
347
- test.ok(sockets[0].destroyed, 'the in-flight socket is destroyed immediately');
348
- test.done();
349
- };
350
-
351
- module.exports['Proxy Connection: a stalled endpoint connection still hits the deadline'] = async test => {
352
- // autoConnect false: the dependency-level connection never completes
353
- const { proxyConnection, sockets, logger } = httpCase({ autoConnect: false });
354
-
355
- let err = await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993, { connectionTimeout: 60 })
356
- .then(() => null)
357
- .catch(e => e);
358
-
359
- test.ok(err, 'a connection that never establishes rejects');
360
- test.equal(err.code, 'CONNECT_TIMEOUT');
361
- test.ok(sockets[0].destroyed, 'the shared deadline destroys the stalled socket');
362
- test.done();
363
- };
364
-
365
- module.exports['Proxy Connection: concurrent HTTP deadlines are independent'] = async test => {
366
- const { proxyConnection, sockets, logger } = httpCase();
367
-
368
- // A short-deadline connection that stalls and a long-deadline connection that succeeds
369
- const stalled = proxyConnection(logger, 'http://proxy.example.com:8080', 'stalled.example.com', 993, { connectionTimeout: 60 })
370
- .then(() => null)
371
- .catch(e => e);
372
- const healthy = proxyConnection(logger, 'http://proxy.example.com:8080', 'healthy.example.com', 993, { connectionTimeout: 5000 });
373
-
374
- await tick();
375
- test.equal(sockets.length, 2, 'both connections opened their own socket');
376
-
377
- let err = await stalled;
378
- test.equal(err.code, 'CONNECT_TIMEOUT', 'the short deadline expired on its own connection');
379
- test.ok(sockets[0].destroyed, 'only the stalled socket was destroyed');
380
- test.ok(!sockets[1].destroyed, 'the other connection is untouched by the expired deadline');
381
-
382
- respondOk(sockets[1]);
383
- const socket = await healthy;
384
- test.equal(socket, sockets[1], 'the longer deadline still completes normally');
385
- test.done();
386
- };
387
-
388
- module.exports['Proxy Connection: an exhausted deadline rejects before any work starts'] = async test => {
389
- const { proxyConnection, calls, logger } = httpCase();
390
-
391
- const deadline = new ConnectionDeadline(50);
392
- await new Promise(resolve => setTimeout(resolve, 80));
393
-
394
- let err = await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993, { deadline })
395
- .then(() => null)
396
- .catch(e => e);
397
-
398
- test.ok(err, 'no phase is started once the budget is gone');
399
- test.equal(err.code, 'CONNECT_TIMEOUT');
400
- test.deepEqual(calls, [], 'no socket was opened');
401
- test.done();
402
- };
403
-
404
- // ============================================
405
- // SOCKS
406
- // ============================================
407
-
408
- const socksCase = async ({ proxyUrl, host, port = 993, resolve4Result, dnsCalls }) => {
409
- const options = [];
410
- const logger = createMockLogger();
411
- const socket = createFakeSocket();
412
-
413
- const { proxyConnection } = loadProxyModule({
414
- socksCreateConnection: async opts => {
415
- options.push(opts);
416
- return { socket };
417
- },
418
- dnsStub: createDnsStub(resolve4Result, dnsCalls ? (method, hostname) => dnsCalls.push([method, hostname]) : null)
419
- });
420
-
421
- const result = await proxyConnection(logger, proxyUrl, host, port).then(
422
- value => ({ value }),
423
- err => ({ err })
424
- );
425
-
426
- return { options, logger, socket, result };
427
- };
428
-
429
- module.exports['Proxy Connection: SOCKS5 preserves the destination hostname for remote DNS'] = async test => {
430
- const dnsCalls = [];
431
- const { options, result } = await socksCase({ proxyUrl: 'socks5://proxy.example.com:1080', host: 'mail.example.com', dnsCalls });
432
-
433
- test.equal(options[0].proxy.type, 5);
434
- test.equal(options[0].proxy.host, 'proxy.example.com', 'the endpoint hostname reaches the dependency unresolved');
435
- test.equal(options[0].destination.host, 'mail.example.com', 'the destination hostname is left for the proxy to resolve');
436
- test.equal(options[0].command, 'connect');
437
- test.ok(options[0].timeout > 0, 'a strictly positive timeout is passed, never zero');
438
- test.deepEqual(dnsCalls, [], 'ImapFlow resolves nothing for SOCKS5');
439
- test.ok(result.value, 'the socket is returned');
440
- test.done();
441
- };
442
-
443
- module.exports['Proxy Connection: SOCKS (alias) defaults to SOCKS5'] = async test => {
444
- const { options } = await socksCase({ proxyUrl: 'socks://proxy.example.com:1080', host: 'mail.example.com' });
445
- test.equal(options[0].proxy.type, 5);
446
- test.equal(options[0].destination.host, 'mail.example.com');
447
- test.done();
448
- };
449
-
450
- module.exports['Proxy Connection: SOCKS5 passes IP literals through unchanged'] = async test => {
451
- let { options } = await socksCase({ proxyUrl: 'socks5://proxy.example.com:1080', host: '192.168.1.1' });
452
- test.equal(options[0].destination.host, '192.168.1.1', 'IPv4 literal unchanged');
453
-
454
- ({ options } = await socksCase({ proxyUrl: 'socks5://proxy.example.com:1080', host: '2001:db8::9' }));
455
- test.equal(options[0].destination.host, '2001:db8::9', 'IPv6 literal unchanged');
456
- test.done();
457
- };
458
-
459
- module.exports['Proxy Connection: SOCKS4a preserves the destination hostname for remote DNS'] = async test => {
460
- const dnsCalls = [];
461
- const { options } = await socksCase({ proxyUrl: 'socks4a://proxy.example.com:1080', host: 'mail.example.com', dnsCalls });
462
-
463
- test.equal(options[0].proxy.type, 4, 'the dependency uses proxy type 4 for both SOCKS4 and SOCKS4a');
464
- test.equal(options[0].destination.host, 'mail.example.com', 'the hostname is preserved so the proxy resolves it');
465
- test.deepEqual(dnsCalls, [], 'no local lookup for SOCKS4a');
466
- test.done();
467
- };
468
-
469
- module.exports['Proxy Connection: SOCKS4 resolves the destination locally to IPv4'] = async test => {
470
- const dnsCalls = [];
471
- const { options } = await socksCase({
472
- proxyUrl: 'socks4://proxy.example.com:1080',
473
- host: 'mail.example.com',
474
- resolve4Result: ['93.184.216.34'],
475
- dnsCalls
476
- });
477
-
478
- test.equal(options[0].proxy.type, 4);
479
- test.equal(options[0].destination.host, '93.184.216.34', 'SOCKS4 gets a resolved IPv4 address, not a hostname');
480
- test.deepEqual(dnsCalls, [['resolve4', 'mail.example.com']], 'the lookup is explicitly IPv4 only');
481
- test.done();
482
- };
483
-
484
- module.exports['Proxy Connection: SOCKS4 reports an unresolvable destination'] = async test => {
485
- const { result } = await socksCase({
486
- proxyUrl: 'socks4://proxy.example.com:1080',
487
- host: 'mail.example.com',
488
- resolve4Result: []
489
- });
490
-
491
- test.ok(result.err, 'an empty IPv4 lookup fails the connection');
492
- test.equal(result.err.code, 'EPROXY');
493
- test.done();
494
- };
495
-
496
- module.exports['Proxy Connection: SOCKS4 and SOCKS4a reject IPv6 destinations clearly'] = async test => {
497
- for (let proxyUrl of ['socks4://proxy.example.com:1080', 'socks4a://proxy.example.com:1080']) {
498
- const { options, result, logger } = await socksCase({ proxyUrl, host: '2001:db8::9' });
499
-
500
- test.ok(result.err, `${proxyUrl} rejects an IPv6 destination`);
501
- test.equal(result.err.code, 'UnsupportedProxyAddress', 'the failure names the unsupported address type');
502
- test.deepEqual(options, [], 'no request is emitted with the address in the wrong field');
503
- test.equal(logger._logs.error.length, 1, 'the failure is logged');
504
- }
505
- test.done();
506
- };
507
-
508
- module.exports['Proxy Connection: bracketed IPv6 proxy URL yields a bare literal endpoint'] = async test => {
509
- const { options, logger } = await socksCase({ proxyUrl: 'socks5://[2001:db8::1]:1080', host: 'mail.example.com' });
510
-
511
- test.equal(options[0].proxy.host, '2001:db8::1', 'brackets are stripped before the address reaches the dependency');
512
- test.ok(logger._logs.info[0].proxyUrl.includes('[2001:db8::1]'), 'logs keep a valid bracketed URL');
513
- test.done();
514
- };
515
-
516
- module.exports['Proxy Connection: SOCKS passes credentials and hides them in logs'] = async test => {
517
- const { options, logger } = await socksCase({ proxyUrl: 'socks5://testuser:testpass@proxy.example.com:1080', host: 'mail.example.com' });
518
-
519
- test.equal(options[0].proxy.userId, 'testuser');
520
- test.equal(options[0].proxy.password, 'testpass');
521
- test.ok(!logger._logs.info[0].proxyUrl.includes('testpass'));
522
- test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
523
- test.done();
524
- };
525
-
526
- module.exports['Proxy Connection: SOCKS with username only'] = async test => {
527
- const { options } = await socksCase({ proxyUrl: 'socks5://testuser@proxy.example.com:1080', host: 'mail.example.com' });
528
- test.equal(options[0].proxy.userId, 'testuser');
529
- test.equal(options[0].proxy.password, '', 'empty string from URL parsing');
530
- test.done();
531
- };
532
-
533
- module.exports['Proxy Connection: SOCKS default port'] = async test => {
534
- const { options } = await socksCase({ proxyUrl: 'socks5://proxy.example.com', host: 'mail.example.com' });
535
- test.equal(options[0].proxy.port, 1080);
536
- test.done();
537
- };
538
-
539
- module.exports['Proxy Connection: SOCKS failure is reported and redacted'] = async test => {
540
- const logger = createMockLogger();
541
- const testError = new Error('SOCKS connection failed');
542
-
543
- const { proxyConnection } = loadProxyModule({
544
- socksCreateConnection: async () => {
545
- throw testError;
546
- }
547
- });
548
-
549
- let err = await proxyConnection(logger, 'socks5://user:secret@proxy.example.com:1080', 'mail.example.com', 993)
550
- .then(() => null)
551
- .catch(e => e);
552
-
553
- test.equal(err, testError, 'the dependency error is preserved');
554
- test.equal(logger._logs.error.length, 1);
555
- test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
556
- test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
557
- test.done();
558
- };
559
-
560
- module.exports['Proxy Connection: SOCKS returning no socket fails'] = async test => {
561
- const logger = createMockLogger();
562
-
563
- const { proxyConnection } = loadProxyModule({ socksCreateConnection: async () => ({}) });
564
-
565
- let err = await proxyConnection(logger, 'socks5://proxy.example.com:1080', 'mail.example.com', 993)
566
- .then(() => null)
567
- .catch(e => e);
568
-
569
- test.ok(err, 'a missing socket is an error rather than an undefined return');
570
- test.equal(err.code, 'EPROXY');
571
- test.done();
572
- };
573
-
574
- module.exports['Proxy Connection: the SOCKS dependency timeout is normalized to CONNECT_TIMEOUT'] = async test => {
575
- const logger = createMockLogger();
576
-
577
- const { proxyConnection } = loadProxyModule({
578
- socksCreateConnection: async () => {
579
- // the shape the socks client uses for its own expiry
580
- throw new Error('Proxy connection timed out');
581
- }
582
- });
583
-
584
- let err = await proxyConnection(logger, 'socks5://proxy.example.com:1080', 'mail.example.com', 993, { connectionTimeout: 5000 })
585
- .then(() => null)
586
- .catch(e => e);
587
-
588
- test.equal(err.code, 'CONNECT_TIMEOUT', 'SOCKS and HTTP expose the same timeout code');
589
- test.equal(err.details.connectionTimeout, 5000, 'the normalized error carries the configured timeout');
590
- test.ok(err._err, 'the dependency error is kept for diagnostics');
591
- test.done();
592
- };
593
-
594
- module.exports['Proxy Connection: a stalled SOCKS negotiation hits the shared deadline'] = async test => {
595
- const logger = createMockLogger();
596
-
597
- const { proxyConnection } = loadProxyModule({
598
- socksCreateConnection: () => new Promise(() => {}) // never settles
599
- });
600
-
601
- let err = await proxyConnection(logger, 'socks5://proxy.example.com:1080', 'mail.example.com', 993, { connectionTimeout: 60 })
602
- .then(() => null)
603
- .catch(e => e);
604
-
605
- test.equal(err.code, 'CONNECT_TIMEOUT', 'the shared deadline bounds the dependency');
606
- test.done();
607
- };
608
-
609
- module.exports['Proxy Connection: SOCKS4 local lookup runs inside the deadline'] = async test => {
610
- const logger = createMockLogger();
611
-
612
- const { proxyConnection } = loadProxyModule({
613
- socksCreateConnection: async () => ({ socket: createFakeSocket() }),
614
- dnsStub: { promises: { resolve4: () => new Promise(() => {}), resolve: () => new Promise(() => {}) } }
615
- });
616
-
617
- let err = await proxyConnection(logger, 'socks4://proxy.example.com:1080', 'mail.example.com', 993, { connectionTimeout: 60 })
618
- .then(() => null)
619
- .catch(e => e);
620
-
621
- test.equal(err.code, 'CONNECT_TIMEOUT', 'a stalled DNS lookup cannot outlive the connection timeout');
622
- test.done();
623
- };
624
-
625
- // ============================================
626
- // Edge cases
627
- // ============================================
628
-
629
- module.exports['Proxy Connection: Unknown protocol returns undefined'] = async test => {
630
- const logger = createMockLogger();
631
- const { proxyConnection } = loadProxyModule({});
632
-
633
- const result = await proxyConnection(logger, 'ftp://proxy.example.com:21', 'mail.example.com', 993);
634
-
635
- test.equal(result, undefined);
636
- test.done();
637
- };
638
-
639
- module.exports['Proxy Connection: detachEarlyErrorHandler removes the early guard'] = async test => {
640
- const { proxyConnection, detachEarlyErrorHandler, sockets, logger } = httpCase();
641
-
642
- const promise = proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
643
- await tick();
644
- respondOk(sockets[0]);
645
- const socket = await promise;
646
-
647
- test.ok(socket.listenerCount('error') >= 1, 'early error handler attached on return');
648
- test.ok(typeof socket._earlyErrorHandler === 'function', 'handler reference stored on the socket');
649
-
650
- detachEarlyErrorHandler(socket);
651
-
652
- test.equal(socket.listenerCount('error'), 0, 'early error handler removed after detach');
653
- test.equal(socket._earlyErrorHandler, null, 'stored handler reference cleared');
654
-
655
- // Detaching again (or on a bare socket) must be a safe no-op.
656
- test.doesNotThrow(() => detachEarlyErrorHandler(socket));
657
- test.doesNotThrow(() => detachEarlyErrorHandler(createFakeSocket()));
658
-
659
- test.done();
660
- };
661
-
662
- module.exports['Proxy Connection: socket without .on skips early error handler attach'] = async test => {
663
- const logger = createMockLogger();
664
- const { proxyConnection } = loadProxyModule({ socksCreateConnection: async () => ({ socket: { write() {} } }) });
665
-
666
- const socket = await proxyConnection(logger, 'socks5://proxy.example.com:1080', 'mail.example.com', 993);
667
-
668
- test.ok(socket, 'a socket-like object without an event emitter interface is still returned');
669
- test.equal(socket._earlyErrorHandler, undefined, 'no handler is attached to it');
670
- test.done();
671
- };
672
-
673
- // ============================================
674
- // Credential handling and malformed userinfo
675
- // ============================================
676
-
677
- module.exports['Proxy Connection: a proxy password with a bare percent sign still connects'] = async test => {
678
- // URL userinfo is percent-encoded, so a bare '%' is not valid encoding. Decoding it must not
679
- // throw out of the connect callback, which would both crash the process and leave the
680
- // connection promise pending forever.
681
- const { proxyConnection, sockets, logger } = httpCase();
682
-
683
- const promise = proxyConnection(logger, 'http://user:p%ss@proxy.example.com:8080', 'mail.example.com', 993);
684
- await tick();
685
-
686
- const authHeader = sockets[0].writes[0].match(/Proxy-Authorization: Basic (\S+)/);
687
- test.ok(authHeader, 'credentials are still sent');
688
- test.equal(Buffer.from(authHeader[1], 'base64').toString(), 'user:p%ss', 'the undecodable value is used as it came in');
689
-
690
- respondOk(sockets[0]);
691
- const socket = await promise;
692
- test.ok(socket, 'the tunnel is established');
693
- test.done();
694
- };
695
-
696
- module.exports['Proxy Connection: a SOCKS failure never carries the proxy password'] = async test => {
697
- // The socks client attaches its full options object - password included - to the errors it
698
- // throws, and a logger that serializes error properties would write it out in clear text.
699
- const logger = createMockLogger();
700
-
701
- const { proxyConnection } = loadProxyModule({
702
- socksCreateConnection: async opts => {
703
- let err = new Error('Socks5 proxy rejected connection - Failure');
704
- err.options = opts; // this is what socks does
705
- throw err;
706
- }
707
- });
708
-
709
- let err = await proxyConnection(logger, 'socks5://user:secretpass@proxy.example.com:1080', 'mail.example.com', 993)
710
- .then(() => null)
711
- .catch(e => e);
712
-
713
- test.ok(err, 'the failure still propagates');
714
- test.equal(err.options, undefined, 'the credential-bearing options object is stripped from the error');
715
- test.ok(!JSON.stringify(logger._logs.error).includes('secretpass'), 'the password does not reach the log');
716
- test.done();
717
- };
718
-
719
- module.exports['Proxy Connection: a normalized SOCKS timeout carries no credentials either'] = async test => {
720
- const logger = createMockLogger();
721
-
722
- const { proxyConnection } = loadProxyModule({
723
- socksCreateConnection: async opts => {
724
- let err = new Error('Proxy connection timed out');
725
- err.options = opts;
726
- throw err;
727
- }
728
- });
729
-
730
- let err = await proxyConnection(logger, 'socks5://user:secretpass@proxy.example.com:1080', 'mail.example.com', 993, { connectionTimeout: 5000 })
731
- .then(() => null)
732
- .catch(e => e);
733
-
734
- test.equal(err.code, 'CONNECT_TIMEOUT');
735
- test.equal(err._err.options, undefined, 'the retained diagnostic error is stripped too');
736
- test.ok(!JSON.stringify(logger._logs.error).includes('secretpass'), 'the password does not reach the log');
737
- test.done();
738
- };