imapflow 1.7.8 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +8 -2
- package/dist/cjs/charsets.d.ts +1 -0
- package/dist/cjs/charsets.js +294 -0
- package/dist/cjs/commands/append.d.ts +22 -0
- package/dist/cjs/commands/append.js +151 -0
- package/dist/cjs/commands/authenticate.d.ts +24 -0
- package/dist/cjs/commands/authenticate.js +223 -0
- package/dist/cjs/commands/capability.d.ts +8 -0
- package/dist/cjs/commands/capability.js +32 -0
- package/dist/cjs/commands/close.d.ts +8 -0
- package/dist/cjs/commands/close.js +39 -0
- package/dist/cjs/commands/compress.d.ts +8 -0
- package/dist/cjs/commands/compress.js +56 -0
- package/dist/cjs/commands/copy.d.ts +13 -0
- package/dist/cjs/commands/copy.js +44 -0
- package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
- package/dist/cjs/commands/copyuid-parser.js +32 -0
- package/dist/cjs/commands/create.d.ts +11 -0
- package/dist/cjs/commands/create.js +80 -0
- package/dist/cjs/commands/delete.d.ts +11 -0
- package/dist/cjs/commands/delete.js +40 -0
- package/dist/cjs/commands/enable.d.ts +9 -0
- package/dist/cjs/commands/enable.js +61 -0
- package/dist/cjs/commands/esearch-parser.d.ts +17 -0
- package/dist/cjs/commands/esearch-parser.js +91 -0
- package/dist/cjs/commands/expunge.d.ts +12 -0
- package/dist/cjs/commands/expunge.js +60 -0
- package/dist/cjs/commands/fetch.d.ts +30 -0
- package/dist/cjs/commands/fetch.js +241 -0
- package/dist/cjs/commands/id.d.ts +10 -0
- package/dist/cjs/commands/id.js +80 -0
- package/dist/cjs/commands/idle.d.ts +9 -0
- package/dist/cjs/commands/idle.js +347 -0
- package/dist/cjs/commands/list.d.ts +16 -0
- package/dist/cjs/commands/list.js +520 -0
- package/dist/cjs/commands/login.d.ts +11 -0
- package/dist/cjs/commands/login.js +42 -0
- package/dist/cjs/commands/logout.d.ts +8 -0
- package/dist/cjs/commands/logout.js +47 -0
- package/dist/cjs/commands/move.d.ts +13 -0
- package/dist/cjs/commands/move.js +57 -0
- package/dist/cjs/commands/namespace.d.ts +25 -0
- package/dist/cjs/commands/namespace.js +139 -0
- package/dist/cjs/commands/noop.d.ts +8 -0
- package/dist/cjs/commands/noop.js +22 -0
- package/dist/cjs/commands/quota.d.ts +10 -0
- package/dist/cjs/commands/quota.js +119 -0
- package/dist/cjs/commands/rename.d.ts +12 -0
- package/dist/cjs/commands/rename.js +48 -0
- package/dist/cjs/commands/search.d.ts +15 -0
- package/dist/cjs/commands/search.js +228 -0
- package/dist/cjs/commands/select.d.ts +25 -0
- package/dist/cjs/commands/select.js +250 -0
- package/dist/cjs/commands/starttls.d.ts +8 -0
- package/dist/cjs/commands/starttls.js +30 -0
- package/dist/cjs/commands/status-fields.d.ts +14 -0
- package/dist/cjs/commands/status-fields.js +61 -0
- package/dist/cjs/commands/status.d.ts +12 -0
- package/dist/cjs/commands/status.js +108 -0
- package/dist/cjs/commands/store.d.ts +19 -0
- package/dist/cjs/commands/store.js +93 -0
- package/dist/cjs/commands/subscribe.d.ts +9 -0
- package/dist/cjs/commands/subscribe.js +31 -0
- package/dist/cjs/commands/unsubscribe.d.ts +9 -0
- package/dist/cjs/commands/unsubscribe.js +31 -0
- package/dist/cjs/connection-deadline.d.ts +49 -0
- package/dist/cjs/connection-deadline.js +91 -0
- package/dist/cjs/errors.d.ts +83 -0
- package/dist/cjs/errors.js +13 -0
- package/dist/cjs/handler/imap-compiler.d.ts +24 -0
- package/dist/cjs/handler/imap-compiler.js +285 -0
- package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/cjs/handler/imap-formal-syntax.js +121 -0
- package/dist/cjs/handler/imap-handler.d.ts +9 -0
- package/dist/cjs/handler/imap-handler.js +10 -0
- package/dist/cjs/handler/imap-parser.d.ts +16 -0
- package/dist/cjs/handler/imap-parser.js +90 -0
- package/dist/cjs/handler/imap-stream.d.ts +181 -0
- package/dist/cjs/handler/imap-stream.js +446 -0
- package/dist/cjs/handler/limits.d.ts +25 -0
- package/dist/cjs/handler/limits.js +51 -0
- package/dist/cjs/handler/parser-instance.d.ts +68 -0
- package/dist/cjs/handler/parser-instance.js +223 -0
- package/dist/cjs/handler/token-parser.d.ts +91 -0
- package/dist/cjs/handler/token-parser.js +673 -0
- package/dist/cjs/handler/types.d.ts +91 -0
- package/dist/cjs/handler/types.js +4 -0
- package/dist/cjs/imap-commands.d.ts +16 -0
- package/dist/cjs/imap-commands.js +74 -0
- package/dist/cjs/imap-flow.d.ts +676 -0
- package/dist/cjs/imap-flow.js +3956 -0
- package/dist/cjs/jp-decoder.d.ts +12 -0
- package/dist/cjs/jp-decoder.js +79 -0
- package/dist/cjs/limited-passthrough.d.ts +25 -0
- package/dist/cjs/limited-passthrough.js +54 -0
- package/dist/cjs/logger.d.ts +3 -0
- package/dist/cjs/logger.js +11 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proxy-connection.d.ts +33 -0
- package/dist/cjs/proxy-connection.js +392 -0
- package/dist/cjs/search-compiler.d.ts +34 -0
- package/dist/cjs/search-compiler.js +476 -0
- package/dist/cjs/special-use.d.ts +22 -0
- package/dist/cjs/special-use.js +911 -0
- package/dist/cjs/tools.d.ts +427 -0
- package/dist/cjs/tools.js +1496 -0
- package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +387 -517
- package/dist/cjs/types.js +5 -0
- package/dist/esm/charsets.d.ts +1 -0
- package/{lib → dist/esm}/charsets.js +1 -6
- package/dist/esm/commands/append.d.ts +22 -0
- package/{lib → dist/esm}/commands/append.js +22 -52
- package/dist/esm/commands/authenticate.d.ts +24 -0
- package/{lib → dist/esm}/commands/authenticate.js +62 -87
- package/dist/esm/commands/capability.d.ts +8 -0
- package/{lib → dist/esm}/commands/capability.js +6 -9
- package/dist/esm/commands/close.d.ts +8 -0
- package/{lib → dist/esm}/commands/close.js +6 -10
- package/dist/esm/commands/compress.d.ts +8 -0
- package/{lib → dist/esm}/commands/compress.js +7 -11
- package/dist/esm/commands/copy.d.ts +13 -0
- package/{lib → dist/esm}/commands/copy.js +12 -20
- package/dist/esm/commands/copyuid-parser.d.ts +11 -0
- package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
- package/dist/esm/commands/create.d.ts +11 -0
- package/{lib → dist/esm}/commands/create.js +13 -27
- package/dist/esm/commands/delete.d.ts +11 -0
- package/{lib → dist/esm}/commands/delete.js +9 -14
- package/dist/esm/commands/enable.d.ts +9 -0
- package/{lib → dist/esm}/commands/enable.js +23 -30
- package/dist/esm/commands/esearch-parser.d.ts +17 -0
- package/dist/esm/commands/esearch-parser.js +88 -0
- package/dist/esm/commands/expunge.d.ts +12 -0
- package/{lib → dist/esm}/commands/expunge.js +17 -22
- package/dist/esm/commands/fetch.d.ts +30 -0
- package/{lib → dist/esm}/commands/fetch.js +32 -64
- package/dist/esm/commands/id.d.ts +10 -0
- package/{lib → dist/esm}/commands/id.js +17 -23
- package/dist/esm/commands/idle.d.ts +9 -0
- package/{lib → dist/esm}/commands/idle.js +47 -81
- package/dist/esm/commands/list.d.ts +16 -0
- package/{lib → dist/esm}/commands/list.js +60 -123
- package/dist/esm/commands/login.d.ts +11 -0
- package/{lib → dist/esm}/commands/login.js +10 -15
- package/dist/esm/commands/logout.d.ts +8 -0
- package/{lib → dist/esm}/commands/logout.js +9 -11
- package/dist/esm/commands/move.d.ts +13 -0
- package/{lib → dist/esm}/commands/move.js +13 -21
- package/dist/esm/commands/namespace.d.ts +25 -0
- package/{lib → dist/esm}/commands/namespace.js +34 -44
- package/dist/esm/commands/noop.d.ts +8 -0
- package/{lib → dist/esm}/commands/noop.js +6 -7
- package/dist/esm/commands/quota.d.ts +10 -0
- package/{lib → dist/esm}/commands/quota.js +18 -36
- package/dist/esm/commands/rename.d.ts +12 -0
- package/{lib → dist/esm}/commands/rename.js +10 -15
- package/dist/esm/commands/search.d.ts +15 -0
- package/{lib → dist/esm}/commands/search.js +36 -135
- package/dist/esm/commands/select.d.ts +25 -0
- package/{lib → dist/esm}/commands/select.js +33 -64
- package/dist/esm/commands/starttls.d.ts +8 -0
- package/{lib → dist/esm}/commands/starttls.js +6 -8
- package/dist/esm/commands/status-fields.d.ts +14 -0
- package/{lib → dist/esm}/commands/status-fields.js +5 -16
- package/dist/esm/commands/status.d.ts +12 -0
- package/{lib → dist/esm}/commands/status.js +18 -29
- package/dist/esm/commands/store.d.ts +19 -0
- package/{lib → dist/esm}/commands/store.js +24 -37
- package/dist/esm/commands/subscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/subscribe.js +8 -12
- package/dist/esm/commands/unsubscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
- package/dist/esm/connection-deadline.d.ts +49 -0
- package/{lib → dist/esm}/connection-deadline.js +14 -25
- package/dist/esm/errors.d.ts +83 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/handler/imap-compiler.d.ts +24 -0
- package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
- package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/esm/handler/imap-formal-syntax.js +117 -0
- package/dist/esm/handler/imap-handler.d.ts +9 -0
- package/dist/esm/handler/imap-handler.js +9 -0
- package/dist/esm/handler/imap-parser.d.ts +16 -0
- package/{lib → dist/esm}/handler/imap-parser.js +31 -44
- package/dist/esm/handler/imap-stream.d.ts +181 -0
- package/{lib → dist/esm}/handler/imap-stream.js +29 -121
- package/dist/esm/handler/limits.d.ts +25 -0
- package/{lib → dist/esm}/handler/limits.js +13 -22
- package/dist/esm/handler/parser-instance.d.ts +68 -0
- package/{lib → dist/esm}/handler/parser-instance.js +19 -47
- package/dist/esm/handler/token-parser.d.ts +91 -0
- package/{lib → dist/esm}/handler/token-parser.js +71 -155
- package/dist/esm/handler/types.d.ts +91 -0
- package/dist/esm/handler/types.js +3 -0
- package/dist/esm/imap-commands.d.ts +16 -0
- package/dist/esm/imap-commands.js +67 -0
- package/dist/esm/imap-flow.d.ts +676 -0
- package/{lib → dist/esm}/imap-flow.js +769 -1790
- package/dist/esm/jp-decoder.d.ts +12 -0
- package/{lib → dist/esm}/jp-decoder.js +6 -21
- package/dist/esm/limited-passthrough.d.ts +25 -0
- package/{lib → dist/esm}/limited-passthrough.js +7 -20
- package/dist/esm/logger.d.ts +3 -0
- package/dist/esm/logger.js +4 -0
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proxy-connection.d.ts +33 -0
- package/{lib → dist/esm}/proxy-connection.js +56 -127
- package/dist/esm/search-compiler.d.ts +34 -0
- package/{lib → dist/esm}/search-compiler.js +54 -110
- package/dist/esm/special-use.d.ts +22 -0
- package/dist/esm/special-use.js +907 -0
- package/dist/esm/tools.d.ts +427 -0
- package/dist/esm/tools.js +1446 -0
- package/dist/esm/types.d.ts +828 -0
- package/dist/esm/types.js +4 -0
- package/package.json +60 -20
- package/.gitattributes +0 -1
- package/.github/CODE_OF_CONDUCT.md +0 -76
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/contributing.md +0 -17
- package/.github/workflows/release.yaml +0 -36
- package/.github/workflows/stale.yml +0 -29
- package/.github/workflows/test.yml +0 -51
- package/.ncurc.js +0 -4
- package/.prettierignore +0 -4
- package/.prettierrc.js +0 -8
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -104
- package/Gruntfile.js +0 -23
- package/eslint.config.js +0 -45
- package/lib/handler/imap-formal-syntax.js +0 -189
- package/lib/handler/imap-handler.js +0 -17
- package/lib/imap-commands.js +0 -45
- package/lib/logger.js +0 -5
- package/lib/special-use.js +0 -923
- package/lib/tools.js +0 -1612
- package/release-please-config.json +0 -10
- package/test/authentication-test.js +0 -101
- package/test/auto-idle-test.js +0 -470
- package/test/bodystructure-test.js +0 -899
- package/test/charsets-test.js +0 -161
- package/test/commands-branches-test.js +0 -1095
- package/test/commands-integration-test.js +0 -11124
- package/test/commands-test.js +0 -73
- package/test/connection-edge-cases-test.js +0 -1828
- package/test/connection-test.js +0 -162
- package/test/copyuid-parser-test.js +0 -173
- package/test/fetch-generator-test.js +0 -218
- package/test/fixtures/fake-timers.js +0 -115
- package/test/fixtures/serialized-mimetorture.js +0 -2738
- package/test/fixtures/test-client.js +0 -101
- package/test/fixtures/test-tls.js +0 -8
- package/test/handler-branches-test.js +0 -310
- package/test/idle-polling-test.js +0 -518
- package/test/imap-compiler-test.js +0 -809
- package/test/imap-flow-compress-test.js +0 -166
- package/test/imap-flow-coverage-test.js +0 -612
- package/test/imap-flow-fetch-download-test.js +0 -909
- package/test/imap-flow-internals-test.js +0 -725
- package/test/imap-flow-methods-test.js +0 -889
- package/test/imap-flow-proxy-paths-test.js +0 -366
- package/test/imap-flow-secure-test.js +0 -573
- package/test/imap-flow-server-test.js +0 -1474
- package/test/imap-formal-syntax-test.js +0 -293
- package/test/imap-parser-test.js +0 -1474
- package/test/imap-stream-edge-cases-test.js +0 -666
- package/test/imap-stream-test.js +0 -177
- package/test/imapflow-test.js +0 -258
- package/test/integration/README.md +0 -52
- package/test/integration/dovecot-test.conf +0 -27
- package/test/integration/rev2-live-test.js +0 -431
- package/test/integration/run-rev2-tests.sh +0 -75
- package/test/integration-test.js +0 -83
- package/test/jp-decoder-test.js +0 -304
- package/test/limited-passthrough-test.js +0 -299
- package/test/memory-cleanup-test.js +0 -144
- package/test/memory-leak-test.js +0 -667
- package/test/parser-limits-test.js +0 -292
- package/test/proxy-connection-test.js +0 -738
- package/test/reliability-improvements-test.js +0 -548
- package/test/search-compiler-test.js +0 -1300
- package/test/search-test.js +0 -329
- package/test/special-use-test.js +0 -418
- package/test/starttls-injection-test.js +0 -181
- package/test/tag-correlation-test.js +0 -333
- package/test/timer-policy-test.js +0 -227
- package/test/token-parser-test.js +0 -456
- package/test/tools-test.js +0 -2013
- 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
|
-
};
|