Skip to content

Commit d789375

Browse files
committed
Allow SOCKS proxies to be chained
1 parent b133295 commit d789375

File tree

2 files changed

+100
-26
lines changed

2 files changed

+100
-26
lines changed

packages/socks-proxy-agent/src/index.ts

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { SocksClient, SocksProxy, SocksClientOptions } from 'socks';
1+
import {
2+
SocksClient,
3+
SocksProxy,
4+
SocksClientOptions,
5+
SocksClientChainOptions,
6+
} from 'socks';
27
import { Agent, AgentConnectOpts } from 'agent-base';
38
import createDebug from 'debug';
49
import * as dns from 'dns';
@@ -87,18 +92,31 @@ export class SocksProxyAgent extends Agent {
8792
'socks5h',
8893
] as const;
8994

90-
readonly shouldLookup: boolean;
91-
readonly proxy: SocksProxy;
95+
readonly shouldLookup!: boolean;
96+
readonly proxies: SocksProxy[];
9297
timeout: number | null;
9398

94-
constructor(uri: string | URL, opts?: SocksProxyAgentOptions) {
99+
constructor(
100+
uri: string | URL | string[] | URL[],
101+
opts?: SocksProxyAgentOptions
102+
) {
95103
super(opts);
96104

97-
const url = typeof uri === 'string' ? new URL(uri) : uri;
98-
const { proxy, lookup } = parseSocksURL(url);
105+
const uri_list = Array.isArray(uri) ? uri : [uri];
106+
107+
if (uri_list.length === 0) {
108+
throw new Error('At least one proxy server URI must be specified.');
109+
}
110+
111+
this.proxies = [];
112+
for (const [i, uri] of uri_list.entries()) {
113+
const { proxy, lookup } = parseSocksURL(new URL(uri.toString()));
114+
this.proxies.push(proxy);
115+
if (i === 0) {
116+
this.shouldLookup = lookup;
117+
}
118+
}
99119

100-
this.shouldLookup = lookup;
101-
this.proxy = proxy;
102120
this.timeout = opts?.timeout ?? null;
103121
}
104122

@@ -110,7 +128,7 @@ export class SocksProxyAgent extends Agent {
110128
req: http.ClientRequest,
111129
opts: AgentConnectOpts
112130
): Promise<net.Socket> {
113-
const { shouldLookup, proxy, timeout } = this;
131+
const { shouldLookup, proxies, timeout } = this;
114132

115133
if (!opts.host) {
116134
throw new Error('No `host` defined!');
@@ -133,25 +151,46 @@ export class SocksProxyAgent extends Agent {
133151
});
134152
}
135153

136-
const socksOpts: SocksClientOptions = {
137-
proxy,
138-
destination: {
139-
host,
140-
port: typeof port === 'number' ? port : parseInt(port, 10),
141-
},
142-
command: 'connect',
143-
timeout: timeout ?? undefined,
144-
};
145-
154+
let socket: net.Socket;
146155
const cleanup = (tlsSocket?: tls.TLSSocket) => {
147156
req.destroy();
148157
socket.destroy();
149158
if (tlsSocket) tlsSocket.destroy();
150159
};
151160

152-
debug('Creating socks proxy connection: %o', socksOpts);
153-
const { socket } = await SocksClient.createConnection(socksOpts);
154-
debug('Successfully created socks proxy connection');
161+
if (proxies.length === 1) {
162+
const socksOpts: SocksClientOptions = {
163+
proxy: proxies[0],
164+
destination: {
165+
host,
166+
port: typeof port === 'number' ? port : parseInt(port, 10),
167+
},
168+
command: 'connect',
169+
timeout: timeout ?? undefined,
170+
};
171+
172+
debug('Creating socks proxy connection: %o', socksOpts);
173+
const connection = await SocksClient.createConnection(socksOpts);
174+
socket = connection.socket;
175+
debug('Successfully created socks proxy connection');
176+
} else {
177+
const socksOpts: SocksClientChainOptions = {
178+
proxies: proxies,
179+
destination: {
180+
host,
181+
port: typeof port === 'number' ? port : parseInt(port, 10),
182+
},
183+
command: 'connect',
184+
timeout: timeout ?? undefined,
185+
};
186+
187+
debug('Creating chained socks proxy connection: %o', socksOpts);
188+
const connection = await SocksClient.createConnectionChain(
189+
socksOpts
190+
);
191+
socket = connection.socket;
192+
debug('Successfully created chained socks proxy connection');
193+
}
155194

156195
if (timeout !== null) {
157196
socket.setTimeout(timeout);

packages/socks-proxy-agent/test/test.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ describe('SocksProxyAgent', () => {
6868
describe('constructor', () => {
6969
it('should accept a "string" proxy argument', () => {
7070
const agent = new SocksProxyAgent(socksServerUrl.href);
71-
assert.equal(socksServerUrl.hostname, agent.proxy.host);
72-
assert.equal(+socksServerUrl.port, agent.proxy.port);
71+
assert.equal(socksServerUrl.hostname, agent.proxies[0].host);
72+
assert.equal(+socksServerUrl.port, agent.proxies[0].port);
7373
});
7474
it('should accept a `new URL()` result object argument', () => {
7575
const agent = new SocksProxyAgent(socksServerUrl);
76-
assert.equal(socksServerUrl.hostname, agent.proxy.host);
77-
assert.equal(+socksServerUrl.port, agent.proxy.port);
76+
assert.equal(socksServerUrl.hostname, agent.proxies[0].host);
77+
assert.equal(+socksServerUrl.port, agent.proxies[0].port);
7878
});
7979
it('should respect `timeout` option during connection to socks server', async () => {
8080
const agent = new SocksProxyAgent(socksServerUrl, { timeout: 1 });
@@ -107,6 +107,41 @@ describe('SocksProxyAgent', () => {
107107
const body = await json(res);
108108
assert.equal('bar', body.foo);
109109
});
110+
111+
it('should work against an HTTP endpoint with multiple SOCKS proxies', async () => {
112+
const secondSocksServer = socks.createServer(function (
113+
// @ts-expect-error no types for `socksv5`
114+
_info,
115+
// @ts-expect-error no types for `socksv5`
116+
accept
117+
) {
118+
accept();
119+
});
120+
await listen(secondSocksServer);
121+
const port = secondSocksServer.address().port;
122+
const secondSocksServerUrl = new URL(`socks://127.0.0.1:${port}`);
123+
secondSocksServer.useAuth(socks.auth.None());
124+
125+
httpServer.once('request', function (req, res) {
126+
assert.equal('/foo', req.url);
127+
res.statusCode = 404;
128+
res.end(JSON.stringify(req.headers));
129+
});
130+
131+
const res = await req(new URL('/foo', httpServerUrl), {
132+
agent: new SocksProxyAgent([
133+
socksServerUrl,
134+
secondSocksServerUrl,
135+
]),
136+
headers: { foo: 'bar' },
137+
});
138+
assert.equal(404, res.statusCode);
139+
140+
const body = await json(res);
141+
assert.equal('bar', body.foo);
142+
143+
secondSocksServer.close();
144+
});
110145
});
111146

112147
describe('"https" module', () => {

0 commit comments

Comments
 (0)