Skip to content

Commit 4195519

Browse files
committed
WIP
1 parent f50e351 commit 4195519

File tree

6 files changed

+109
-30
lines changed

6 files changed

+109
-30
lines changed

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

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { get as httpsGet } from 'https';
55
import { expect } from 'chai';
66
import sinon from 'sinon';
77
import { HTTPServerProxyTestSetup } from '../test/helpers';
8-
import socks5AuthNone from 'socksv5/lib/auth/None';
9-
import socks5AuthUserPassword from 'socksv5/lib/auth/UserPassword';
108

119
describe('createAgent', function () {
1210
let setup: HTTPServerProxyTestSetup;
@@ -47,7 +45,7 @@ describe('createAgent', function () {
4745

4846
context('socks5', function () {
4947
it('can connect to a socks5 proxy without auth', async function () {
50-
setup.socks5ProxyServer.useAuth(socks5AuthNone());
48+
setup.socks5AuthNone();
5149

5250
const res = await get(
5351
'http://example.com/hello',
@@ -61,7 +59,7 @@ describe('createAgent', function () {
6159

6260
it('can connect to a socks5 proxy with successful auth', async function () {
6361
const authHandler = sinon.stub().yields(true);
64-
setup.socks5ProxyServer.useAuth(socks5AuthUserPassword(authHandler));
62+
setup.socks5AuthUsernamePassword(authHandler);
6563

6664
const res = await get(
6765
'http://example.com/hello',
@@ -78,7 +76,7 @@ describe('createAgent', function () {
7876

7977
it('fails to connect to a socks5 proxy with unsuccessful auth', async function () {
8078
const authHandler = sinon.stub().yields(false);
81-
setup.socks5ProxyServer.useAuth(socks5AuthUserPassword(authHandler));
79+
setup.socks5AuthUsernamePassword(authHandler);
8280

8381
try {
8482
await get(
@@ -95,7 +93,7 @@ describe('createAgent', function () {
9593
});
9694

9795
context('http proxy', function () {
98-
it('can connect to a socks5 proxy without auth', async function () {
96+
it('can connect to a http proxy without auth', async function () {
9997
const res = await get(
10098
'http://example.com/hello',
10199
createAgent({ proxy: `http://127.0.0.1:${setup.httpProxyPort}` })
@@ -106,7 +104,7 @@ describe('createAgent', function () {
106104
]);
107105
});
108106

109-
it('can connect to a socks5 proxy with successful auth', async function () {
107+
it('can connect to a http proxy with successful auth', async function () {
110108
setup.authHandler = sinon.stub().returns(true);
111109

112110
const res = await get(
@@ -122,7 +120,7 @@ describe('createAgent', function () {
122120
expect(setup.authHandler).to.have.been.calledOnceWith('foo', 'bar');
123121
});
124122

125-
it('fails to connect to a socks5 proxy with unsuccessful auth', async function () {
123+
it('fails to connect to a http proxy with unsuccessful auth', async function () {
126124
setup.authHandler = sinon.stub().returns(false);
127125

128126
const res = await get(
@@ -147,7 +145,7 @@ describe('createAgent', function () {
147145
]);
148146
});
149147

150-
it('can connect to a socks5 proxy with successful auth', async function () {
148+
it('can connect to a https proxy with successful auth', async function () {
151149
setup.authHandler = sinon.stub().returns(true);
152150

153151
const res = await get(
@@ -163,7 +161,7 @@ describe('createAgent', function () {
163161
expect(setup.authHandler).to.have.been.calledOnceWith('foo', 'bar');
164162
});
165163

166-
it('fails to connect to a socks5 proxy with unsuccessful auth', async function () {
164+
it('fails to connect to a https proxy with unsuccessful auth', async function () {
167165
setup.authHandler = sinon.stub().returns(false);
168166

169167
const res = await get(
@@ -177,25 +175,25 @@ describe('createAgent', function () {
177175
});
178176

179177
context('ssh proxy', function () {
180-
it('can connect to a ssh proxy without auth', async function () {
178+
it('can connect to an ssh proxy without auth', async function () {
181179
const res = await get(
182-
'https://example.com/hello',
180+
'http://example.com/hello',
183181
createAgent({ proxy: `ssh://[email protected]:${setup.sshProxyPort}` })
184182
);
185183
expect(res.body).to.equal('OK /hello');
186184
expect(setup.getRequestedUrls()).to.deep.equal([
187185
'http://example.com/hello',
188186
]);
189187
expect(setup.sshTunnelInfos).to.deep.equal([
190-
{ destIP: 'example.com', destPort: 0, srcIP: '127.0.0.1', srcPort: 0 },
188+
{ destIP: 'example.com', destPort: 80, srcIP: '127.0.0.1', srcPort: 0 },
191189
]);
192190
});
193191

194-
it('can connect to a ssh proxy with successful auth', async function () {
192+
it('can connect to an ssh proxy with successful auth', async function () {
195193
setup.authHandler = sinon.stub().returns(true);
196194

197195
const res = await get(
198-
'https://example.com/hello',
196+
'http://example.com/hello',
199197
createAgent({ proxy: `ssh://foo:[email protected]:${setup.sshProxyPort}` })
200198
);
201199
expect(res.body).to.equal('OK /hello');
@@ -205,7 +203,7 @@ describe('createAgent', function () {
205203
expect(setup.authHandler).to.have.been.calledOnceWith('foo', 'bar');
206204
});
207205

208-
it('fails to connect to a ssh proxy with unsuccessful auth', async function () {
206+
it('fails to connect to an ssh proxy with unsuccessful auth', async function () {
209207
setup.authHandler = sinon.stub().returns(false);
210208

211209
try {
@@ -223,7 +221,7 @@ describe('createAgent', function () {
223221
}
224222
});
225223

226-
it('fails to connect to a ssh proxy with unavailable tunneling', async function () {
224+
it('fails to connect to an ssh proxy with unavailable tunneling', async function () {
227225
setup.authHandler = sinon.stub().returns(true);
228226
setup.canTunnel = false;
229227

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function createAgent(
3939
const getProxyForUrl = proxyForUrl(proxyOptions);
4040
return new ProxyAgent({
4141
getProxyForUrl,
42+
...proxyOptions,
4243
});
4344
}
4445

packages/devtools-proxy-support/src/fetch.spec.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createFetch } from './';
33
import { HTTPServerProxyTestSetup } from '../test/helpers';
44

55
describe('createFetch', function () {
6-
it(`consistency check: plain "import('node-fetch') fails"`, async function () {
6+
it("consistency check: plain `import('node-fetch')` fails", async function () {
77
let failed = false;
88
try {
99
await import('node-fetch');
@@ -17,12 +17,12 @@ describe('createFetch', function () {
1717
context('HTTP calls', function () {
1818
let setup: HTTPServerProxyTestSetup;
1919

20-
before(async function () {
20+
beforeEach(async function () {
2121
setup = new HTTPServerProxyTestSetup();
2222
await setup.listen();
2323
});
2424

25-
after(async function () {
25+
afterEach(async function () {
2626
await setup.teardown();
2727
});
2828

@@ -33,12 +33,58 @@ describe('createFetch', function () {
3333
expect(await response.text()).to.equal('OK /test');
3434
});
3535

36-
it.only('makes use of proxy support when instructed to do so', async function () {
37-
const response = await createFetch({
36+
it('makes use of SSH proxy support when instructed to do so', async function () {
37+
const fetch = createFetch({
3838
proxy: `ssh://[email protected]:${setup.sshProxyPort}`,
39-
})(`http://127.0.0.1:${setup.httpServerPort}/test`);
39+
});
40+
const response = await fetch(
41+
`http://localhost:${setup.httpServerPort}/test`
42+
);
43+
expect(await response.text()).to.equal('OK /test');
44+
expect(setup.sshTunnelInfos).to.deep.equal([
45+
{
46+
destIP: 'localhost',
47+
destPort: setup.httpServerPort,
48+
srcIP: '127.0.0.1',
49+
srcPort: 0,
50+
},
51+
]);
52+
fetch.agent?.destroy?.();
53+
});
54+
55+
it('makes use of HTTP proxy support when instructed to do so', async function () {
56+
const fetch = createFetch({
57+
proxy: `http://127.0.0.1:${setup.httpProxyPort}`,
58+
});
59+
const response = await fetch(
60+
`http://localhost:${setup.httpServerPort}/test`
61+
);
62+
expect(await response.text()).to.equal('OK /test');
63+
fetch.agent?.destroy?.();
64+
});
65+
66+
it('makes use of HTTPS proxy support when instructed to do so', async function () {
67+
const fetch = createFetch({
68+
proxy: `http://127.0.0.1:${setup.httpsProxyPort}`,
69+
ca: setup.tlsOptions.ca,
70+
});
71+
const response = await fetch(
72+
`https://localhost:${setup.httpsServerPort}/test`
73+
);
74+
expect(await response.text()).to.equal('OK /test');
75+
fetch.agent?.destroy?.();
76+
});
77+
78+
it('makes use of Socks5 proxy support when instructed to do so', async function () {
79+
setup.socks5AuthNone();
80+
const fetch = createFetch({
81+
proxy: `socks5://127.0.0.1:${setup.socks5ProxyPort}`,
82+
});
83+
const response = await fetch(
84+
`http://localhost:${setup.httpServerPort}/test`
85+
);
4086
expect(await response.text()).to.equal('OK /test');
41-
expect(setup.sshTunnelInfos).to.deep.equal([]);
87+
fetch.agent?.destroy?.();
4288
});
4389
});
4490
});

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import type { ConnectionOptions } from 'tls';
2+
13
// Should be an opaque type, but TS does not support those.
24
export type DevtoolsProxyOptionsSecrets = string;
35

4-
export interface DevtoolsProxyOptions {
6+
export type DevtoolsProxyOptions = {
57
// Can be an ssh://, socks5://, http://, https:// or pac<...>:// URL
68
// Everything besides ssh:// gets forwarded to the `proxy-agent` npm package
79
proxy?: string;
@@ -14,7 +16,10 @@ export interface DevtoolsProxyOptions {
1416
identityKeyFile?: string;
1517
identityKeyPassphrase?: string;
1618
};
17-
}
19+
} & Pick<
20+
ConnectionOptions,
21+
'ca' | 'cert' | 'crl' | 'key' | 'passphrase' | 'pfx'
22+
>;
1823

1924
// https://www.electronjs.org/docs/latest/api/structures/proxy-config
2025
interface ElectronProxyConfig {

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import EventEmitter, { once } from 'events';
1010
import { promises as fs } from 'fs';
1111
import { promisify } from 'util';
1212
import type { ProxyLogEmitter } from './logging';
13+
import { connect as tlsConnect } from 'tls';
14+
import type { Socket } from 'net';
1315

1416
export class SSHAgent extends AgentBase implements AgentWithInitialize {
1517
public logger: ProxyLogEmitter;
@@ -68,6 +70,7 @@ export class SSHAgent extends AgentBase implements AgentWithInitialize {
6870
? await fs.readFile(this.proxyOptions.sshOptions.identityKeyFile)
6971
: undefined,
7072
passphrase: this.proxyOptions.sshOptions?.identityKeyPassphrase,
73+
// debug: console.log.bind(null, '[client]')
7174
};
7275

7376
this.logger.emit('ssh:establishing-conection', {
@@ -125,7 +128,20 @@ export class SSHAgent extends AgentBase implements AgentWithInitialize {
125128

126129
await this.initialize();
127130

128-
return await this.forwardOut('127.0.0.1', 0, url.hostname, +url.port);
131+
let sock: Duplex & Partial<Pick<Socket, 'setTimeout'>> =
132+
await this.forwardOut('127.0.0.1', 0, url.hostname, +url.port);
133+
(sock as any).setTimeout ??= function () {
134+
// noop, required for node-fetch
135+
return this;
136+
};
137+
if (connectOpts.secureEndpoint) {
138+
sock = tlsConnect({
139+
...this.proxyOptions,
140+
...connectOpts,
141+
socket: sock,
142+
});
143+
}
144+
return sock;
129145
} catch (err: unknown) {
130146
const retryableError = (err as Error).message === 'Not connected';
131147
this.logger.emit('ssh:failed-forward', {

packages/devtools-proxy-support/test/helpers.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { createServer as createHTTPServer, get as httpGet } from 'http';
1313
import type { TcpipRequestInfo } from 'ssh2';
1414
import { Server as SSHServer } from 'ssh2';
1515
import DuplexPair from 'duplexpair';
16+
import { promisify } from 'util';
1617

1718
import socks5Server from 'socksv5/lib/server';
18-
import { promisify } from 'util';
19+
import socks5AuthNone from 'socksv5/lib/auth/None';
20+
import socks5AuthUserPassword from 'socksv5/lib/auth/UserPassword';
1921

2022
function parseHTTPAuthHeader(header: string | undefined): [string, string] {
2123
if (!header?.startsWith('Basic ')) return ['', ''];
@@ -129,6 +131,7 @@ export class HTTPServerProxyTestSetup {
129131
this.sshServer = new SSHServer(
130132
{
131133
hostKeys: [this.tlsOptions.sshdKey],
134+
// debug: console.log.bind(null, '[server]')
132135
},
133136
(client) => {
134137
client
@@ -167,7 +170,17 @@ export class HTTPServerProxyTestSetup {
167170
);
168171
}
169172

170-
getRequestedUrls() {
173+
socks5AuthNone(): void {
174+
this.socks5ProxyServer.useAuth(socks5AuthNone());
175+
}
176+
177+
socks5AuthUsernamePassword(
178+
cb: (user: string, pass: string, cb: (success: boolean) => void) => void
179+
): void {
180+
this.socks5ProxyServer.useAuth(socks5AuthUserPassword(cb));
181+
}
182+
183+
getRequestedUrls(): string[] {
171184
return this.requests.map((r) =>
172185
Object.assign(new URL(`http://_`), {
173186
pathname: r.url,
@@ -176,7 +189,7 @@ export class HTTPServerProxyTestSetup {
176189
);
177190
}
178191

179-
async teardown() {
192+
async teardown(): Promise<void> {
180193
const closePromises: Promise<unknown>[] = [];
181194
for (const server of [
182195
this.httpServer,

0 commit comments

Comments
 (0)