Skip to content

Commit d38eddb

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

File tree

3 files changed

+113
-22
lines changed

3 files changed

+113
-22
lines changed

.changeset/plenty-lamps-tease.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'socks-proxy-agent': minor
3+
---
4+
5+
You can now use SOCKS proxy chains by passing an array of SOCKS proxy URLs to the `SocksProxyAgent()` constructor:
6+
7+
```ts
8+
const agent = new SocksProxyAgent([
9+
'socks://user:pass@host:port',
10+
'socks://user:pass@host:port',
11+
'socks://user:pass@host:port',
12+
]);
13+
```

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

Lines changed: 65 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,21 +92,38 @@ 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 uriList = Array.isArray(uri) ? uri : [uri];
106+
107+
if (uriList.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 uriList.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

123+
get proxy(): SocksProxy {
124+
return this.proxies[0];
125+
}
126+
105127
/**
106128
* Initiates a SOCKS connection to the specified SOCKS proxy server,
107129
* which in turn connects to the specified remote host and port.
@@ -110,7 +132,7 @@ export class SocksProxyAgent extends Agent {
110132
req: http.ClientRequest,
111133
opts: AgentConnectOpts
112134
): Promise<net.Socket> {
113-
const { shouldLookup, proxy, timeout } = this;
135+
const { shouldLookup, proxies, timeout } = this;
114136

115137
if (!opts.host) {
116138
throw new Error('No `host` defined!');
@@ -133,25 +155,46 @@ export class SocksProxyAgent extends Agent {
133155
});
134156
}
135157

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-
158+
let socket: net.Socket;
146159
const cleanup = (tlsSocket?: tls.TLSSocket) => {
147160
req.destroy();
148161
socket.destroy();
149162
if (tlsSocket) tlsSocket.destroy();
150163
};
151164

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

156199
if (timeout !== null) {
157200
socket.setTimeout(timeout);

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)