Skip to content

Commit 01b018d

Browse files
committed
WIP
1 parent 85ad716 commit 01b018d

File tree

14 files changed

+1094
-21
lines changed

14 files changed

+1094
-21
lines changed

package-lock.json

Lines changed: 308 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/devtools-proxy-support/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"proxy-agent": "^6.4.0",
5050
"agent-base": "^7.1.1",
5151
"ssh2": "^1.15.0",
52-
"socksv5": "^0.0.6"
52+
"socksv5": "^0.0.6",
53+
"node-fetch": "^3.3.2"
5354
},
5455
"devDependencies": {
5556
"@mongodb-js/eslint-config-devtools": "0.9.10",
@@ -62,6 +63,7 @@
6263
"@types/sinon-chai": "^3.2.5",
6364
"chai": "^4.3.6",
6465
"depcheck": "^1.4.1",
66+
"duplexpair": "^1.0.2",
6567
"eslint": "^7.25.0",
6668
"gen-esm-wrapper": "^1.1.0",
6769
"mocha": "^8.4.0",
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { createAgent } from './';
2+
import type { Agent, IncomingMessage } from 'http';
3+
import { get as httpGet } from 'http';
4+
import { get as httpsGet } from 'https';
5+
import { expect } from 'chai';
6+
import sinon from 'sinon';
7+
import { HTTPServerProxyTestSetup } from '../test/helpers';
8+
import socks5AuthNone from 'socksv5/lib/auth/None';
9+
import socks5AuthUserPassword from 'socksv5/lib/auth/UserPassword';
10+
11+
describe('createAgent', function () {
12+
let setup: HTTPServerProxyTestSetup;
13+
let agents: Agent[];
14+
15+
const get = async (
16+
url: string,
17+
agent: Agent
18+
): Promise<IncomingMessage & { body: string }> => {
19+
const getFn = new URL(url).protocol === 'https:' ? httpsGet : httpGet;
20+
const options = {
21+
agent,
22+
ca: setup.tlsOptions.ca,
23+
checkServerIdentity: () => undefined, // allow hostname mismatches
24+
};
25+
agents.push(agent);
26+
const res = await new Promise<IncomingMessage>((resolve, reject) =>
27+
getFn(url, options, resolve).once('error', reject)
28+
);
29+
let body = '';
30+
res.setEncoding('utf8');
31+
for await (const chunk of res) body += chunk;
32+
return Object.assign(res, { body });
33+
};
34+
35+
beforeEach(async function () {
36+
agents = [];
37+
setup = new HTTPServerProxyTestSetup();
38+
await setup.listen();
39+
});
40+
41+
afterEach(async function () {
42+
await setup.teardown();
43+
for (const agent of new Set(agents)) {
44+
agent.destroy();
45+
}
46+
});
47+
48+
context('socks5', function () {
49+
it('can connect to a socks5 proxy without auth', async function () {
50+
setup.socks5ProxyServer.useAuth(socks5AuthNone());
51+
52+
const res = await get(
53+
'http://example.com/hello',
54+
createAgent({ proxy: `socks5://127.0.0.1:${setup.socks5ProxyPort}` })
55+
);
56+
expect(res.body).to.equal('OK /hello');
57+
expect(setup.getRequestedUrls()).to.deep.equal([
58+
'http://example.com/hello',
59+
]);
60+
});
61+
62+
it('can connect to a socks5 proxy with successful auth', async function () {
63+
const authHandler = sinon.stub().yields(true);
64+
setup.socks5ProxyServer.useAuth(socks5AuthUserPassword(authHandler));
65+
66+
const res = await get(
67+
'http://example.com/hello',
68+
createAgent({
69+
proxy: `socks5://foo:[email protected]:${setup.socks5ProxyPort}`,
70+
})
71+
);
72+
expect(res.body).to.equal('OK /hello');
73+
expect(setup.getRequestedUrls()).to.deep.equal([
74+
'http://example.com/hello',
75+
]);
76+
expect(authHandler).to.have.been.calledOnceWith('foo', 'bar');
77+
});
78+
79+
it('fails to connect to a socks5 proxy with unsuccessful auth', async function () {
80+
const authHandler = sinon.stub().yields(false);
81+
setup.socks5ProxyServer.useAuth(socks5AuthUserPassword(authHandler));
82+
83+
try {
84+
await get(
85+
'http://example.com/hello',
86+
createAgent({
87+
proxy: `socks5://foo:[email protected]:${setup.socks5ProxyPort}`,
88+
})
89+
);
90+
expect.fail('missed exception');
91+
} catch (err: any) {
92+
expect(err.message).to.equal('Socks5 Authentication failed');
93+
}
94+
});
95+
});
96+
97+
context('http proxy', function () {
98+
it('can connect to a socks5 proxy without auth', async function () {
99+
const res = await get(
100+
'http://example.com/hello',
101+
createAgent({ proxy: `http://127.0.0.1:${setup.httpProxyPort}` })
102+
);
103+
expect(res.body).to.equal('OK /hello');
104+
expect(setup.getRequestedUrls()).to.deep.equal([
105+
'http://example.com/hello',
106+
]);
107+
});
108+
109+
it('can connect to a socks5 proxy with successful auth', async function () {
110+
setup.authHandler = sinon.stub().returns(true);
111+
112+
const res = await get(
113+
'http://example.com/hello',
114+
createAgent({
115+
proxy: `http://foo:[email protected]:${setup.httpProxyPort}`,
116+
})
117+
);
118+
expect(res.body).to.equal('OK /hello');
119+
expect(setup.getRequestedUrls()).to.deep.equal([
120+
'http://example.com/hello',
121+
]);
122+
expect(setup.authHandler).to.have.been.calledOnceWith('foo', 'bar');
123+
});
124+
125+
it('fails to connect to a socks5 proxy with unsuccessful auth', async function () {
126+
setup.authHandler = sinon.stub().returns(false);
127+
128+
const res = await get(
129+
'http://example.com/hello',
130+
createAgent({
131+
proxy: `http://foo:[email protected]:${setup.httpProxyPort}`,
132+
})
133+
);
134+
expect(res.statusCode).to.equal(407);
135+
});
136+
});
137+
138+
context('https/connect proxy', function () {
139+
it('can connect to a https proxy without auth', async function () {
140+
const res = await get(
141+
'https://example.com/hello',
142+
createAgent({ proxy: `http://127.0.0.1:${setup.httpsProxyPort}` })
143+
);
144+
expect(res.body).to.equal('OK /hello');
145+
expect(setup.getRequestedUrls()).to.deep.equal([
146+
'http://example.com/hello',
147+
]);
148+
});
149+
150+
it('can connect to a socks5 proxy with successful auth', async function () {
151+
setup.authHandler = sinon.stub().returns(true);
152+
153+
const res = await get(
154+
'https://example.com/hello',
155+
createAgent({
156+
proxy: `http://foo:[email protected]:${setup.httpsProxyPort}`,
157+
})
158+
);
159+
expect(res.body).to.equal('OK /hello');
160+
expect(setup.getRequestedUrls()).to.deep.equal([
161+
'http://example.com/hello',
162+
]);
163+
expect(setup.authHandler).to.have.been.calledOnceWith('foo', 'bar');
164+
});
165+
166+
it('fails to connect to a socks5 proxy with unsuccessful auth', async function () {
167+
setup.authHandler = sinon.stub().returns(false);
168+
169+
const res = await get(
170+
'https://example.com/hello',
171+
createAgent({
172+
proxy: `http://foo:[email protected]:${setup.httpsProxyPort}`,
173+
})
174+
);
175+
expect(res.statusCode).to.equal(407);
176+
});
177+
});
178+
179+
context('ssh proxy', function () {
180+
it('can connect to a ssh proxy without auth', async function () {
181+
const res = await get(
182+
'https://example.com/hello',
183+
createAgent({ proxy: `ssh://[email protected]:${setup.sshProxyPort}` })
184+
);
185+
expect(res.body).to.equal('OK /hello');
186+
expect(setup.getRequestedUrls()).to.deep.equal([
187+
'http://example.com/hello',
188+
]);
189+
expect(setup.sshTunnelInfos).to.deep.equal([
190+
{ destIP: 'example.com', destPort: 0, srcIP: '127.0.0.1', srcPort: 0 },
191+
]);
192+
});
193+
194+
it('can connect to a ssh proxy with successful auth', async function () {
195+
setup.authHandler = sinon.stub().returns(true);
196+
197+
const res = await get(
198+
'https://example.com/hello',
199+
createAgent({ proxy: `ssh://foo:[email protected]:${setup.sshProxyPort}` })
200+
);
201+
expect(res.body).to.equal('OK /hello');
202+
expect(setup.getRequestedUrls()).to.deep.equal([
203+
'http://example.com/hello',
204+
]);
205+
expect(setup.authHandler).to.have.been.calledOnceWith('foo', 'bar');
206+
});
207+
208+
it('fails to connect to a ssh proxy with unsuccessful auth', async function () {
209+
setup.authHandler = sinon.stub().returns(false);
210+
211+
try {
212+
await get(
213+
'http://example.com/hello',
214+
createAgent({
215+
proxy: `ssh://foo:[email protected]:${setup.sshProxyPort}`,
216+
})
217+
);
218+
expect.fail('missed exception');
219+
} catch (err: any) {
220+
expect(err.message).to.equal(
221+
'All configured authentication methods failed'
222+
);
223+
}
224+
});
225+
226+
it('fails to connect to a ssh proxy with unavailable tunneling', async function () {
227+
setup.authHandler = sinon.stub().returns(true);
228+
setup.canTunnel = false;
229+
230+
try {
231+
await get(
232+
'http://example.com/hello',
233+
createAgent({
234+
proxy: `ssh://foo:[email protected]:${setup.sshProxyPort}`,
235+
})
236+
);
237+
expect.fail('missed exception');
238+
} catch (err: any) {
239+
expect(err.message).to.include('Channel open failure');
240+
}
241+
});
242+
});
243+
});

packages/devtools-proxy-support/src/agent.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export type AgentWithInitialize = Agent & {
3030
export function createAgent(
3131
proxyOptions: DevtoolsProxyOptions
3232
): AgentWithInitialize {
33+
// This could be made a bit more flexible by creating an Agent using AgentBase
34+
// that will dynamically choose between SSHAgent and ProxyAgent.
35+
// Right now, this is a bit simpler in terms of lifetime management for SSHAgent.
3336
if (proxyOptions.proxy && new URL(proxyOptions.proxy).protocol === 'ssh:') {
3437
return new SSHAgent(proxyOptions);
3538
}
@@ -38,3 +41,19 @@ export function createAgent(
3841
getProxyForUrl,
3942
});
4043
}
44+
45+
export function useOrCreateAgent(
46+
proxyOptions: DevtoolsProxyOptions | AgentWithInitialize,
47+
target?: string
48+
): AgentWithInitialize | undefined {
49+
if ('createConnection' in proxyOptions) {
50+
return proxyOptions as AgentWithInitialize;
51+
} else {
52+
if (
53+
target !== undefined &&
54+
!proxyForUrl(proxyOptions as DevtoolsProxyOptions)(target)
55+
)
56+
return undefined;
57+
return createAgent(proxyOptions as DevtoolsProxyOptions);
58+
}
59+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { expect } from 'chai';
2+
import { createFetch } from './';
3+
import { HTTPServerProxyTestSetup } from '../test/helpers';
4+
5+
describe('createFetch', function () {
6+
it(`consistency check: plain "import('node-fetch') fails"`, async function () {
7+
let failed = false;
8+
try {
9+
await import('node-fetch');
10+
} catch (error) {
11+
failed = true;
12+
expect((error as Error).message).to.include('require() of ES Module');
13+
}
14+
expect(failed).to.equal(true);
15+
});
16+
17+
context('HTTP calls', function () {
18+
let setup: HTTPServerProxyTestSetup;
19+
20+
before(async function () {
21+
setup = new HTTPServerProxyTestSetup();
22+
await setup.listen();
23+
});
24+
25+
after(async function () {
26+
await setup.teardown();
27+
});
28+
29+
it('provides a node-fetch-like HTTP functionality', async function () {
30+
const response = await createFetch({})(
31+
`http://127.0.0.1:${setup.httpServerPort}/test`
32+
);
33+
expect(await response.text()).to.equal('OK /test');
34+
});
35+
36+
it.only('makes use of proxy support when instructed to do so', async function () {
37+
const response = await createFetch({
38+
proxy: `ssh://[email protected]:${setup.sshProxyPort}`,
39+
})(`http://127.0.0.1:${setup.httpServerPort}/test`);
40+
expect(await response.text()).to.equal('OK /test');
41+
expect(setup.sshTunnelInfos).to.deep.equal([]);
42+
});
43+
});
44+
});

0 commit comments

Comments
 (0)