Skip to content

Commit a76bdc6

Browse files
committed
WIP
1 parent 3454f2f commit a76bdc6

File tree

7 files changed

+106
-7
lines changed

7 files changed

+106
-7
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type AgentWithInitialize = Agent & {
1818
// first before starting to push connections through it)
1919
initialize?(): Promise<void>;
2020
logger?: ProxyLogEmitter;
21+
readonly proxyOptions?: Readonly<DevtoolsProxyOptions>;
2122

2223
// This is just part of the regular Agent interface, used by Node.js itself,
2324
// but missing from @types/node
@@ -40,10 +41,13 @@ export function createAgent(
4041
return new SSHAgent(proxyOptions);
4142
}
4243
const getProxyForUrl = proxyForUrl(proxyOptions);
43-
return new ProxyAgent({
44-
getProxyForUrl,
45-
...proxyOptions,
46-
});
44+
return Object.assign(
45+
new ProxyAgent({
46+
getProxyForUrl,
47+
...proxyOptions,
48+
}),
49+
{ proxyOptions }
50+
);
4751
}
4852

4953
export function useOrCreateAgent(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export {
66
mergeProxySecrets,
77
} from './proxy-options';
88
export { Tunnel, TunnelOptions, setupSocks5Tunnel } from './socks5';
9-
export { createAgent } from './agent';
9+
export { createAgent, useOrCreateAgent, AgentWithInitialize } from './agent';
1010
export {
1111
createFetch,
1212
Request,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,18 @@ interface MongoLogWriter {
7474
mongoLogId(this: void, id: number): unknown;
7575
}
7676

77+
let alreadyHooked: WeakMap<ProxyLogEmitter, WeakSet<MongoLogWriter>>;
7778
let idCounter = 0;
7879
export function hookLogger(
7980
emitter: ProxyLogEmitter,
8081
logCtx: string,
8182
log: MongoLogWriter
8283
): void {
84+
// This helps avoid unintentionally attaching the same logging twice in devtools-connet
85+
if (alreadyHooked.get(emitter)?.has(log)) return;
86+
if (!alreadyHooked.has(emitter)) alreadyHooked.set(emitter, new WeakSet());
87+
alreadyHooked.get(emitter)!.add(log);
88+
8389
logCtx = `${logCtx}-${idCounter++}`;
8490
const { mongoLogId } = log;
8591

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ConnectionOptions } from 'tls';
2+
import type { TunnelOptions } from './socks5';
23

34
// Should be an opaque type, but TS does not support those.
45
export type DevtoolsProxyOptionsSecrets = string;
@@ -216,6 +217,27 @@ export function translateToElectronProxyConfig(
216217
return {};
217218
}
218219

220+
// Return the Socks5 tunnel configuration, if proxyOptions always resolves to one.
221+
// This is used by setupSocks5Tunnel() to avoid creating a local Socks5 tunnel
222+
// that would just forward to another Socks5 tunnel anyway.
223+
export function getSocks5OnlyProxyOptions(
224+
proxyOptions: DevtoolsProxyOptions,
225+
target?: string
226+
): TunnelOptions | undefined {
227+
let proxyUrl: string | undefined;
228+
if (target !== undefined) proxyUrl = proxyForUrl(proxyOptions)(target);
229+
else if (!proxyOptions.noProxyHosts) proxyUrl = proxyOptions.proxy;
230+
if (!proxyUrl) return undefined;
231+
const url = new URL(proxyUrl);
232+
if (url.protocol !== 'socks5:') return undefined;
233+
return {
234+
proxyHost: decodeURIComponent(url.hostname),
235+
proxyPort: +(url.port || 1080),
236+
proxyUsername: decodeURIComponent(url.username) || undefined,
237+
proxyPassword: decodeURIComponent(url.password) || undefined,
238+
};
239+
}
240+
219241
interface DevtoolsProxyOptionsSecretsInternal {
220242
username?: string;
221243
password?: string;

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import sinon from 'sinon';
22
import { HTTPServerProxyTestSetup } from '../test/helpers';
3-
import type { Tunnel } from './socks5';
3+
import type { Tunnel, TunnelOptions } from './socks5';
44
import { setupSocks5Tunnel } from './socks5';
55
import { expect } from 'chai';
66
import { createFetch } from './fetch';
7+
import type { DevtoolsProxyOptions } from './proxy-options';
78

89
describe('setupSocks5Tunnel', function () {
910
let setup: HTTPServerProxyTestSetup;
@@ -97,4 +98,45 @@ describe('setupSocks5Tunnel', function () {
9798
expect(err.code).to.equal('EADDRNOTAVAIL');
9899
}
99100
});
101+
102+
it('does not start an actual server if the proxy config already specifies socks5', async function () {
103+
async function existingTunnelConfig(
104+
options: DevtoolsProxyOptions,
105+
target?: string
106+
): Promise<TunnelOptions | undefined> {
107+
const tunnel = await setupSocks5Tunnel(options, undefined, target);
108+
expect(tunnel?.constructor.name).to.equal('ExistingTunnel');
109+
return JSON.parse(JSON.stringify(tunnel?.config)); // filter out undefined values
110+
}
111+
112+
expect(
113+
await existingTunnelConfig({ proxy: 'socks5://example.com:123' })
114+
).to.deep.equal({ proxyHost: 'example.com', proxyPort: 123 });
115+
expect(
116+
await existingTunnelConfig({ proxy: 'socks5://example.com' })
117+
).to.deep.equal({ proxyHost: 'example.com', proxyPort: 1080 });
118+
expect(
119+
await existingTunnelConfig({ proxy: 'socks5://foo:[email protected]' })
120+
).to.deep.equal({
121+
proxyHost: 'example.com',
122+
proxyPort: 1080,
123+
proxyUsername: 'foo',
124+
proxyPassword: 'bar',
125+
});
126+
expect(
127+
await existingTunnelConfig(
128+
{ proxy: 'socks5://example.com:123' },
129+
'mongodb://'
130+
)
131+
).to.deep.equal({ proxyHost: 'example.com', proxyPort: 123 });
132+
expect(
133+
await existingTunnelConfig(
134+
{
135+
useEnvironmentVariableProxies: true,
136+
env: { MONGODB_PROXY: 'socks5://example.com:123' },
137+
},
138+
'mongodb://'
139+
)
140+
).to.deep.equal({ proxyHost: 'example.com', proxyPort: 123 });
141+
});
100142
});

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventEmitter, once } from 'events';
2+
import { getSocks5OnlyProxyOptions } from './proxy-options';
23
import type { DevtoolsProxyOptions } from './proxy-options';
34
import type { AgentWithInitialize } from './agent';
45
import { useOrCreateAgent } from './agent';
@@ -287,11 +288,35 @@ class Socks5Server extends EventEmitter implements Tunnel {
287288
}
288289
}
289290

291+
class ExistingTunnel extends EventEmitter {
292+
logger = new EventEmitter();
293+
readonly config: TunnelOptions;
294+
295+
constructor(config: TunnelOptions) {
296+
super();
297+
this.config = config;
298+
}
299+
300+
async close() {
301+
// nothing to do if we didn't start a server
302+
}
303+
}
304+
290305
export async function setupSocks5Tunnel(
291306
proxyOptions: DevtoolsProxyOptions | AgentWithInitialize,
292307
tunnelOptions?: Partial<TunnelOptions>,
293308
target?: string | undefined
294309
): Promise<Tunnel | undefined> {
310+
const socks5OnlyProxyOptions = getSocks5OnlyProxyOptions(
311+
('proxyOptions' in proxyOptions
312+
? proxyOptions.proxyOptions
313+
: proxyOptions) as DevtoolsProxyOptions,
314+
target
315+
);
316+
if (socks5OnlyProxyOptions) {
317+
return new ExistingTunnel(socks5OnlyProxyOptions);
318+
}
319+
295320
const agent = useOrCreateAgent(proxyOptions, target);
296321
if (!agent) return undefined;
297322

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { Socket } from 'net';
1717
// https://github.com/mongodb-js/compass/tree/55a5a608713d7316d158dc66febeb6b114d8b40d/packages/ssh-tunnel/src
1818
export class SSHAgent extends AgentBase implements AgentWithInitialize {
1919
public logger: ProxyLogEmitter;
20-
private readonly proxyOptions: Readonly<DevtoolsProxyOptions>;
20+
public readonly proxyOptions: Readonly<DevtoolsProxyOptions>;
2121
private readonly url: URL;
2222
private sshClient: SshClient;
2323
private connected = false;

0 commit comments

Comments
 (0)