imapflow 1.4.9 → 1.6.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 (48) hide show
  1. package/.github/workflows/test.yml +20 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/CLAUDE.md +3 -5
  5. package/lib/commands/fetch.js +18 -14
  6. package/lib/commands/idle.js +197 -104
  7. package/lib/commands/list.js +19 -8
  8. package/lib/commands/quota.js +3 -0
  9. package/lib/commands/select.js +5 -0
  10. package/lib/commands/status.js +10 -1
  11. package/lib/connection-deadline.js +98 -0
  12. package/lib/handler/imap-compiler.js +20 -14
  13. package/lib/handler/imap-stream.js +141 -50
  14. package/lib/handler/limits.js +43 -0
  15. package/lib/handler/token-parser.js +38 -1
  16. package/lib/imap-flow.d.ts +47 -5
  17. package/lib/imap-flow.js +594 -283
  18. package/lib/proxy-connection.js +393 -98
  19. package/lib/special-use.js +660 -51
  20. package/lib/tools.js +52 -3
  21. package/package.json +2 -2
  22. package/test/commands-branches-test.js +17 -1
  23. package/test/commands-integration-test.js +353 -2
  24. package/test/connection-edge-cases-test.js +4 -40
  25. package/test/fixtures/fake-timers.js +115 -0
  26. package/test/handler-branches-test.js +4 -28
  27. package/test/idle-polling-test.js +349 -0
  28. package/test/imap-compiler-test.js +85 -0
  29. package/test/imap-flow-compress-test.js +12 -0
  30. package/test/imap-flow-coverage-test.js +3 -3
  31. package/test/imap-flow-fetch-download-test.js +56 -0
  32. package/test/imap-flow-internals-test.js +23 -0
  33. package/test/imap-flow-proxy-paths-test.js +151 -0
  34. package/test/imap-flow-secure-test.js +182 -9
  35. package/test/imap-flow-server-test.js +229 -0
  36. package/test/imap-parser-test.js +112 -1
  37. package/test/imap-stream-test.js +46 -0
  38. package/test/integration/README.md +17 -5
  39. package/test/integration/rev2-live-test.js +125 -0
  40. package/test/integration/run-rev2-tests.sh +14 -0
  41. package/test/parser-limits-test.js +274 -0
  42. package/test/proxy-connection-test.js +553 -442
  43. package/test/reliability-improvements-test.js +87 -0
  44. package/test/search-compiler-test.js +17 -0
  45. package/test/special-use-test.js +337 -0
  46. package/test/tag-correlation-test.js +333 -0
  47. package/test/timer-policy-test.js +214 -0
  48. package/test/tools-test.js +42 -4
@@ -1,20 +1,67 @@
1
1
  'use strict';
2
2
 
3
- const httpProxyClient = require('nodemailer/lib/smtp-connection/http-proxy-client');
4
3
  const { SocksClient } = require('socks');
5
- const util = require('util');
6
- const httpProxyClientAsync = util.promisify(httpProxyClient);
7
4
  const dns = require('dns').promises;
8
5
  const net = require('net');
6
+ const tls = require('tls');
9
7
 
10
- // Redacts the password from a parsed URL object before it is logged,
11
- // preventing credentials from appearing in log output.
12
- const hidePassword = proxyUrl => {
13
- if (proxyUrl.password) {
14
- proxyUrl.password = '(hidden)';
8
+ const { ConnectionDeadline } = require('./connection-deadline');
9
+
10
+ // Cap the CONNECT response buffered before the header terminator, so a proxy that never sends
11
+ // \r\n\r\n cannot grow memory without bound.
12
+ const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
13
+
14
+ const DEFAULT_SOCKS_PORT = 1080;
15
+
16
+ // URL hostnames keep the brackets around an IPv6 literal ("[2001:db8::1]"), which is neither a
17
+ // valid input for net.isIP() nor an address net/tls/socks can connect to. Strip them for socket
18
+ // options; the parsed URL itself stays intact for logging and credentials.
19
+ const unbracketAddress = host => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
20
+
21
+ // CONNECT request lines and Host headers need an IPv6 destination wrapped in brackets. Hostnames
22
+ // and IPv4 literals are used as-is, and an already bracketed literal is not bracketed twice.
23
+ const formatAuthority = (host, port) => {
24
+ let address = unbracketAddress(host);
25
+ return net.isIPv6(address) ? `[${address}]:${port}` : `${address}:${port}`;
26
+ };
27
+
28
+ // Password-free rendering of the proxy URL, used in every log path. The caller's URL object is
29
+ // left untouched so credentials stay available for authentication.
30
+ const redactUrl = proxyUrl => {
31
+ let redacted = new URL(proxyUrl.href);
32
+ if (redacted.password) {
33
+ redacted.password = '(hidden)';
34
+ }
35
+ return redacted.href;
36
+ };
37
+
38
+ const proxyError = (message, code) => {
39
+ let err = new Error(message);
40
+ err.code = code || 'ProxyError';
41
+ return err;
42
+ };
43
+
44
+ // URL userinfo is percent-encoded, so it has to be decoded before it can be used as credentials.
45
+ // A password containing a bare '%' is not valid percent-encoding and makes decodeURIComponent
46
+ // throw, so such values are used as they came in rather than failing the connection.
47
+ const decodeUserInfo = value => {
48
+ try {
49
+ return decodeURIComponent(value);
50
+ } catch {
51
+ return value;
15
52
  }
16
53
  };
17
54
 
55
+ // The socks client attaches its full options object - proxy password included - to the errors it
56
+ // throws. Any logger that serializes error properties would then write that password out in clear
57
+ // text, so the credentials are dropped before the error is logged or handed to the caller.
58
+ const stripProxyCredentials = err => {
59
+ if (err && typeof err === 'object' && err.options) {
60
+ delete err.options;
61
+ }
62
+ return err;
63
+ };
64
+
18
65
  // Attaches a benign 'error' listener as soon as the proxied socket exists, so an early
19
66
  // socket error (before ImapFlow installs its own handlers) cannot surface as an unhandled
20
67
  // 'error' event and crash the process. The handler is stored on the socket so the caller
@@ -38,113 +85,361 @@ const detachEarlyErrorHandler = socket => {
38
85
  }
39
86
  };
40
87
 
41
- const proxyConnection = async (logger, connectionUrl, host, port) => {
42
- let proxyUrl = new URL(connectionUrl);
88
+ /**
89
+ * Establishes a tunnel through an HTTP or HTTPS proxy with a CONNECT request.
90
+ *
91
+ * ImapFlow owns this instead of using a bundled helper because the connection-wide deadline has
92
+ * to apply here: the socket is retained as soon as it exists, so an expiry destroys the in-flight
93
+ * socket immediately, and concurrent connections cannot share a single process-global timeout.
94
+ *
95
+ * The destination hostname is passed through unresolved - resolving it is the proxy's job, which
96
+ * is also what keeps DNS traffic off the client for HTTP proxies.
97
+ *
98
+ * @param {Object} params
99
+ * @param {Object} params.logger Logger instance.
100
+ * @param {URL} params.proxyUrl Parsed proxy URL (credentials intact).
101
+ * @param {Boolean} params.secureProxy Whether the proxy endpoint itself speaks TLS.
102
+ * @param {String} params.proxyHost Proxy endpoint host, IPv6 literals unbracketed.
103
+ * @param {Number} params.proxyPort Proxy endpoint port.
104
+ * @param {String} params.host Destination host (hostname or IP literal).
105
+ * @param {Number} params.port Destination port.
106
+ * @param {ConnectionDeadline} params.deadline Shared connection deadline.
107
+ * @returns {Promise<Object>} The established socket, tunnelled to the destination.
108
+ */
109
+ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort, host, port, deadline }) => {
110
+ // Reject CRLF in the destination before it reaches the CONNECT request line and Host header.
111
+ // A tainted host/port could otherwise inject additional headers (HTTP request splitting).
112
+ let destinationPort = Number(port) || 0;
113
+ if (!destinationPort || /[\r\n]/.test(host)) {
114
+ throw proxyError('Invalid proxy destination', 'EPROXY');
115
+ }
43
116
 
44
- let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
117
+ let authority = formatAuthority(host, destinationPort);
45
118
 
46
- // Pre-resolve the IMAP server hostname to an IP address before passing it to the proxy.
47
- // Some proxy implementations (especially SOCKS4) do not support hostname resolution,
48
- // so we resolve DNS on the client side to ensure compatibility.
49
- if (!net.isIP(host)) {
50
- let resolveResult = await dns.resolve(host);
51
- if (resolveResult && resolveResult.length) {
52
- host = resolveResult[0];
53
- }
119
+ let remaining = deadline.remaining();
120
+ if (!remaining) {
121
+ throw deadline.error();
54
122
  }
55
123
 
56
- switch (protocol) {
57
- // Connect using a HTTP CONNECT method
58
- case 'http':
59
- case 'https': {
60
- try {
61
- let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
62
- if (socket) {
63
- hidePassword(proxyUrl);
64
- logger.info({
65
- msg: 'Established a socket via HTTP proxy',
66
- proxyUrl: proxyUrl.href,
67
- port,
68
- host
69
- });
70
- attachEarlyErrorHandler(logger, socket);
124
+ let socket = null;
125
+
126
+ return await new Promise((resolve, reject) => {
127
+ let settled = false;
128
+ let timer = null;
129
+ let headers = '';
130
+
131
+ const onSocketData = chunk => {
132
+ // Scan only the newly arrived bytes (plus the 3 that a terminator could straddle),
133
+ // so a proxy that dribbles its headers cannot turn this into a quadratic rescan.
134
+ let searchFrom = Math.max(0, headers.length - 3);
135
+ headers += chunk.toString('binary');
136
+
137
+ let terminator = headers.indexOf('\r\n\r\n', searchFrom);
138
+ if (terminator < 0) {
139
+ if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
140
+ fail(proxyError('Proxy response headers too large', 'EPROXY'));
71
141
  }
72
- return socket;
73
- } catch (err) {
74
- hidePassword(proxyUrl);
75
- logger.error({
76
- msg: 'Failed to establish a socket via HTTP proxy',
77
- proxyUrl: proxyUrl.href,
78
- port,
79
- host,
80
- err
81
- });
82
- throw err;
142
+ return;
143
+ }
144
+
145
+ // The header block is complete, so this listener must stop consuming before anything
146
+ // is put back: unshifting while still subscribed re-emits the data straight back into
147
+ // this handler, which would swallow it. Pausing hands the socket over cleanly - the
148
+ // next owner resumes it (ImapFlow pipes it into the parser).
149
+ socket.removeListener('data', onSocketData);
150
+ socket.pause();
151
+
152
+ // Anything after the header terminator already belongs to the tunnelled stream (a
153
+ // server greeting that the proxy coalesced with its own response) and has to be
154
+ // preserved for the next consumer. It is put back as the original bytes, taken from
155
+ // this chunk rather than round-tripped through a string.
156
+ let headerBytes = terminator + 4;
157
+ let consumedFromChunk = chunk.length - (headers.length - headerBytes);
158
+ if (consumedFromChunk < chunk.length) {
159
+ socket.unshift(chunk.subarray(consumedFromChunk));
83
160
  }
161
+ headers = headers.slice(0, terminator);
162
+
163
+ let status = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
164
+ if (!status || (status[1] || '').charAt(0) !== '2') {
165
+ return fail(proxyError(`Invalid response from proxy${status ? `: ${status[1]}` : ''}`, 'EPROXY'));
166
+ }
167
+
168
+ succeed();
169
+ };
170
+
171
+ // Single settlement path: temporary listeners and the deadline timer are dropped exactly
172
+ // once, so a late socket event cannot settle the promise twice or leave a timer armed.
173
+ const cleanup = () => {
174
+ clearTimeout(timer);
175
+ timer = null;
176
+ if (socket) {
177
+ // Every temporary listener goes, the connect callback included: after settlement
178
+ // no socket event may run any of this again.
179
+ socket.removeListener('connect', onConnected);
180
+ socket.removeListener('data', onSocketData);
181
+ socket.removeListener('error', fail);
182
+ socket.removeListener('close', onEarlyClose);
183
+ }
184
+ };
185
+
186
+ function fail(err) {
187
+ if (settled) {
188
+ return;
189
+ }
190
+ settled = true;
191
+ cleanup();
192
+ if (socket) {
193
+ socket.destroy();
194
+ }
195
+ reject(err);
84
196
  }
85
197
 
86
- // SOCKS proxy
87
- case 'socks':
88
- case 'socks5':
89
- case 'socks4':
90
- case 'socks4a': {
91
- let proxyType = Number(protocol.replace(/\D/g, '')) || 5;
92
-
93
- // targetHost here is the SOCKS proxy server's hostname (not the final IMAP destination).
94
- // The SOCKS library needs a resolved IP for the proxy host it connects to.
95
- // The final IMAP destination (host/port) is passed separately as 'destination'.
96
- let targetHost = proxyUrl.hostname;
97
- if (!net.isIP(targetHost)) {
98
- let resolveResult = await dns.resolve(targetHost);
99
- if (resolveResult && resolveResult.length) {
100
- targetHost = resolveResult[0];
101
- }
198
+ function succeed() {
199
+ settled = true;
200
+ cleanup();
201
+ resolve(socket);
202
+ }
203
+
204
+ function onEarlyClose() {
205
+ fail(proxyError('Proxy closed the connection before the tunnel was established', 'EPROXY'));
206
+ }
207
+
208
+ timer = setTimeout(() => fail(deadline.error()), remaining);
209
+
210
+ let connectOptions = { host: proxyHost, port: proxyPort };
211
+ if (secureProxy) {
212
+ // Verify the proxy's certificate (Node default) and target SNI plus hostname
213
+ // verification at the proxy endpoint rather than the IMAP destination. An IP-literal
214
+ // endpoint gets no servername, which would be an invalid SNI value.
215
+ if (!net.isIP(proxyHost)) {
216
+ connectOptions.servername = proxyHost;
102
217
  }
218
+ }
103
219
 
104
- let connectionOpts = {
105
- proxy: {
106
- host: targetHost,
107
- port: Number(proxyUrl.port) || 1080,
108
- type: proxyType
109
- },
110
- destination: {
111
- host,
112
- port
113
- },
114
- command: 'connect',
115
- set_tcp_nodelay: true
220
+ // Declared as a function so cleanup() above can detach it (the connect callback is
221
+ // registered as a one-shot 'connect' listener by net/tls).
222
+ function onConnected() {
223
+ let requestHeaders = {
224
+ Host: authority,
225
+ Connection: 'close'
116
226
  };
117
227
 
118
228
  if (proxyUrl.username || proxyUrl.password) {
119
- connectionOpts.proxy.userId = proxyUrl.username;
120
- connectionOpts.proxy.password = proxyUrl.password;
229
+ let credentials = `${decodeUserInfo(proxyUrl.username)}:${decodeUserInfo(proxyUrl.password)}`;
230
+ requestHeaders['Proxy-Authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`;
121
231
  }
122
232
 
123
- try {
124
- const info = await SocksClient.createConnection(connectionOpts);
125
- if (info && info.socket) {
126
- hidePassword(proxyUrl);
127
- logger.info({
128
- msg: 'Established a socket via SOCKS proxy',
129
- proxyUrl: proxyUrl.href,
130
- port,
131
- host
132
- });
133
- attachEarlyErrorHandler(logger, info.socket);
134
- }
135
- return info.socket;
136
- } catch (err) {
137
- hidePassword(proxyUrl);
138
- logger.error({
139
- msg: 'Failed to establish a socket via SOCKS proxy',
140
- proxyUrl: proxyUrl.href,
141
- port,
142
- host,
143
- err
144
- });
145
- throw err;
233
+ socket.write(
234
+ `CONNECT ${authority} HTTP/1.1\r\n` +
235
+ Object.keys(requestHeaders)
236
+ .map(key => `${key}: ${requestHeaders[key]}`)
237
+ .join('\r\n') +
238
+ '\r\n\r\n'
239
+ );
240
+
241
+ socket.on('data', onSocketData);
242
+ }
243
+
244
+ // The socket is retained as soon as it is created, so an expiry can destroy it at once.
245
+ socket = secureProxy ? tls.connect(connectOptions, onConnected) : net.connect(connectOptions, onConnected);
246
+ socket.once('error', fail);
247
+ socket.once('close', onEarlyClose);
248
+ })
249
+ .then(established => {
250
+ logger.info({
251
+ msg: `Established a socket via HTTP proxy`,
252
+ proxyUrl: redactUrl(proxyUrl),
253
+ port,
254
+ host
255
+ });
256
+ attachEarlyErrorHandler(logger, established);
257
+ return established;
258
+ })
259
+ .catch(err => {
260
+ logger.error({
261
+ msg: 'Failed to establish a socket via HTTP proxy',
262
+ proxyUrl: redactUrl(proxyUrl),
263
+ port,
264
+ host,
265
+ err
266
+ });
267
+ throw err;
268
+ });
269
+ };
270
+
271
+ /**
272
+ * Resolves a destination hostname to an IPv4 address. Only used for SOCKS4, which has no IPv6
273
+ * destination address type and no hostname form of its own.
274
+ *
275
+ * @param {String} hostname Destination hostname.
276
+ * @param {ConnectionDeadline} deadline Shared connection deadline.
277
+ * @returns {Promise<String>} An IPv4 address.
278
+ */
279
+ const resolveIPv4 = async (hostname, deadline) => {
280
+ let addresses = await deadline.race(dns.resolve4(hostname));
281
+ if (!addresses || !addresses.length) {
282
+ throw proxyError(`Could not resolve an IPv4 address for ${hostname}`, 'EPROXY');
283
+ }
284
+ return addresses[0];
285
+ };
286
+
287
+ /**
288
+ * Establishes a tunnel through a SOCKS proxy.
289
+ *
290
+ * DNS policy per protocol, because the `socks` client picks the SOCKS4 or SOCKS4a wire format from
291
+ * the destination value alone and offers no switch of its own:
292
+ * * SOCKS4 - destination hostnames are resolved locally to IPv4. Passing a hostname would
293
+ * silently produce a SOCKS4a request that a plain SOCKS4 proxy cannot answer.
294
+ * * SOCKS4a - destination hostnames are preserved for remote DNS.
295
+ * * SOCKS5 - destination hostnames are preserved for remote DNS, IP literals pass through.
296
+ * IPv6 destination literals are rejected for both SOCKS4 and SOCKS4a: neither can carry them, and
297
+ * the dependency would write the literal into the SOCKS4a hostname field instead.
298
+ *
299
+ * @param {Object} params
300
+ * @param {Object} params.logger Logger instance.
301
+ * @param {URL} params.proxyUrl Parsed proxy URL (credentials intact).
302
+ * @param {String} params.protocol Configured proxy protocol (socks, socks4, socks4a, socks5).
303
+ * @param {String} params.proxyHost Proxy endpoint host, IPv6 literals unbracketed.
304
+ * @param {Number} params.proxyPort Proxy endpoint port.
305
+ * @param {String} params.host Destination host (hostname or IP literal).
306
+ * @param {Number} params.port Destination port.
307
+ * @param {ConnectionDeadline} params.deadline Shared connection deadline.
308
+ * @returns {Promise<Object>} The established socket, tunnelled to the destination.
309
+ */
310
+ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort, host, port, deadline }) => {
311
+ let proxyType = protocol === 'socks4' || protocol === 'socks4a' ? 4 : 5;
312
+ let destinationHost = unbracketAddress(host);
313
+
314
+ try {
315
+ if (proxyType === 4) {
316
+ if (net.isIPv6(destinationHost)) {
317
+ throw proxyError(`SOCKS4 and SOCKS4a cannot address IPv6 destinations (${destinationHost})`, 'UnsupportedProxyAddress');
146
318
  }
319
+
320
+ if (protocol === 'socks4' && !net.isIP(destinationHost)) {
321
+ destinationHost = await resolveIPv4(destinationHost, deadline);
322
+ }
323
+ }
324
+
325
+ let connectionOpts = {
326
+ proxy: {
327
+ // The endpoint is handed to net.Socket.connect() by the dependency, so a hostname
328
+ // is left unresolved and gets Node's normal lookup and connection behavior.
329
+ host: proxyHost,
330
+ port: proxyPort,
331
+ type: proxyType
332
+ },
333
+ destination: {
334
+ host: destinationHost,
335
+ port
336
+ },
337
+ command: 'connect',
338
+ set_tcp_nodelay: true
339
+ };
340
+
341
+ if (proxyUrl.username || proxyUrl.password) {
342
+ connectionOpts.proxy.userId = proxyUrl.username;
343
+ connectionOpts.proxy.password = proxyUrl.password;
344
+ }
345
+
346
+ // The dependency treats a zero timeout as its own 30 second default, so only a strictly
347
+ // positive remaining budget may be passed.
348
+ let remaining = deadline.remaining();
349
+ if (!remaining) {
350
+ throw deadline.error();
351
+ }
352
+ connectionOpts.timeout = remaining;
353
+
354
+ const info = await deadline.race(SocksClient.createConnection(connectionOpts));
355
+ if (!info || !info.socket) {
356
+ throw proxyError('SOCKS proxy did not return a socket', 'EPROXY');
147
357
  }
358
+
359
+ logger.info({
360
+ msg: 'Established a socket via SOCKS proxy',
361
+ proxyUrl: redactUrl(proxyUrl),
362
+ port,
363
+ host
364
+ });
365
+ attachEarlyErrorHandler(logger, info.socket);
366
+
367
+ return info.socket;
368
+ } catch (caught) {
369
+ // A dependency expiry and the shared deadline are reported with the same
370
+ // CONNECT_TIMEOUT shape, so a caller does not need to know which noticed first.
371
+ let err = deadline.normalize(stripProxyCredentials(caught));
372
+ stripProxyCredentials(err._err);
373
+
374
+ logger.error({
375
+ msg: 'Failed to establish a socket via SOCKS proxy',
376
+ proxyUrl: redactUrl(proxyUrl),
377
+ port,
378
+ host,
379
+ err
380
+ });
381
+ throw err;
382
+ }
383
+ };
384
+
385
+ /**
386
+ * Opens a socket to `host`:`port` through the configured proxy.
387
+ *
388
+ * @param {Object} logger Logger instance.
389
+ * @param {String} connectionUrl Proxy URL (http, https, socks, socks4, socks4a, socks5).
390
+ * @param {String} host Destination host, passed through unresolved wherever the proxy protocol
391
+ * can resolve it itself.
392
+ * @param {Number} port Destination port.
393
+ * @param {Object} [options]
394
+ * @param {ConnectionDeadline} [options.deadline] Shared connection deadline. Proxy DNS and
395
+ * negotiation run inside it, so a stalled proxy cannot exceed the configured connectionTimeout.
396
+ * @param {Number} [options.connectionTimeout] Used to build a deadline when none was passed.
397
+ * @returns {Promise<Object|undefined>} The tunnelled socket, or undefined for an unknown protocol.
398
+ */
399
+ const proxyConnection = async (logger, connectionUrl, host, port, options) => {
400
+ options = options || {};
401
+
402
+ let deadline = options.deadline || new ConnectionDeadline(options.connectionTimeout);
403
+ deadline.check();
404
+
405
+ let proxyUrl = new URL(connectionUrl);
406
+ let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
407
+
408
+ // ImapFlow performs no DNS lookup of its own for the proxy endpoint: net, tls and the SOCKS
409
+ // client all resolve a hostname endpoint themselves, which keeps Node's normal connection
410
+ // behavior (including address-family selection) instead of pinning one address.
411
+ let proxyHost = unbracketAddress(proxyUrl.hostname);
412
+
413
+ switch (protocol) {
414
+ // Connect using a HTTP CONNECT method
415
+ case 'http':
416
+ case 'https':
417
+ return await httpConnect({
418
+ logger,
419
+ proxyUrl,
420
+ secureProxy: protocol === 'https',
421
+ proxyHost,
422
+ proxyPort: Number(proxyUrl.port) || (protocol === 'https' ? 443 : 80),
423
+ host,
424
+ port,
425
+ deadline
426
+ });
427
+
428
+ // SOCKS proxy
429
+ case 'socks':
430
+ case 'socks5':
431
+ case 'socks4':
432
+ case 'socks4a':
433
+ return await socksConnect({
434
+ logger,
435
+ proxyUrl,
436
+ protocol,
437
+ proxyHost,
438
+ proxyPort: Number(proxyUrl.port) || DEFAULT_SOCKS_PORT,
439
+ host,
440
+ port,
441
+ deadline
442
+ });
148
443
  }
149
444
  };
150
445