Skip to content

Commit

Permalink
Allow SOCKS proxies to be chained
Browse files Browse the repository at this point in the history
  • Loading branch information
codetheweb committed Nov 18, 2023
1 parent b133295 commit d38eddb
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 22 deletions.
13 changes: 13 additions & 0 deletions .changeset/plenty-lamps-tease.md
@@ -0,0 +1,13 @@
---
'socks-proxy-agent': minor
---

You can now use SOCKS proxy chains by passing an array of SOCKS proxy URLs to the `SocksProxyAgent()` constructor:

```ts
const agent = new SocksProxyAgent([
'socks://user:pass@host:port',
'socks://user:pass@host:port',
'socks://user:pass@host:port',
]);
```
87 changes: 65 additions & 22 deletions packages/socks-proxy-agent/src/index.ts
@@ -1,4 +1,9 @@
import { SocksClient, SocksProxy, SocksClientOptions } from 'socks';
import {
SocksClient,
SocksProxy,
SocksClientOptions,
SocksClientChainOptions,
} from 'socks';
import { Agent, AgentConnectOpts } from 'agent-base';
import createDebug from 'debug';
import * as dns from 'dns';
Expand Down Expand Up @@ -87,21 +92,38 @@ export class SocksProxyAgent extends Agent {
'socks5h',
] as const;

readonly shouldLookup: boolean;
readonly proxy: SocksProxy;
readonly shouldLookup!: boolean;
readonly proxies: SocksProxy[];
timeout: number | null;

constructor(uri: string | URL, opts?: SocksProxyAgentOptions) {
constructor(
uri: string | URL | string[] | URL[],
opts?: SocksProxyAgentOptions
) {
super(opts);

const url = typeof uri === 'string' ? new URL(uri) : uri;
const { proxy, lookup } = parseSocksURL(url);
const uriList = Array.isArray(uri) ? uri : [uri];

if (uriList.length === 0) {
throw new Error('At least one proxy server URI must be specified.');
}

this.proxies = [];
for (const [i, uri] of uriList.entries()) {
const { proxy, lookup } = parseSocksURL(new URL(uri.toString()));
this.proxies.push(proxy);
if (i === 0) {
this.shouldLookup = lookup;
}
}

this.shouldLookup = lookup;
this.proxy = proxy;
this.timeout = opts?.timeout ?? null;
}

get proxy(): SocksProxy {
return this.proxies[0];
}

/**
* Initiates a SOCKS connection to the specified SOCKS proxy server,
* which in turn connects to the specified remote host and port.
Expand All @@ -110,7 +132,7 @@ export class SocksProxyAgent extends Agent {
req: http.ClientRequest,
opts: AgentConnectOpts
): Promise<net.Socket> {
const { shouldLookup, proxy, timeout } = this;
const { shouldLookup, proxies, timeout } = this;

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

const socksOpts: SocksClientOptions = {
proxy,
destination: {
host,
port: typeof port === 'number' ? port : parseInt(port, 10),
},
command: 'connect',
timeout: timeout ?? undefined,
};

let socket: net.Socket;
const cleanup = (tlsSocket?: tls.TLSSocket) => {
req.destroy();
socket.destroy();
if (tlsSocket) tlsSocket.destroy();
};

debug('Creating socks proxy connection: %o', socksOpts);
const { socket } = await SocksClient.createConnection(socksOpts);
debug('Successfully created socks proxy connection');
if (proxies.length === 1) {
const socksOpts: SocksClientOptions = {
proxy: proxies[0],
destination: {
host,
port: typeof port === 'number' ? port : parseInt(port, 10),
},
command: 'connect',
timeout: timeout ?? undefined,
};

debug('Creating socks proxy connection: %o', socksOpts);
const connection = await SocksClient.createConnection(socksOpts);
socket = connection.socket;
debug('Successfully created socks proxy connection');
} else {
const socksOpts: SocksClientChainOptions = {
proxies: proxies,
destination: {
host,
port: typeof port === 'number' ? port : parseInt(port, 10),
},
command: 'connect',
timeout: timeout ?? undefined,
};

debug('Creating chained socks proxy connection: %o', socksOpts);
const connection = await SocksClient.createConnectionChain(
socksOpts
);
socket = connection.socket;
debug('Successfully created chained socks proxy connection');
}

if (timeout !== null) {
socket.setTimeout(timeout);
Expand Down
35 changes: 35 additions & 0 deletions packages/socks-proxy-agent/test/test.ts
Expand Up @@ -107,6 +107,41 @@ describe('SocksProxyAgent', () => {
const body = await json(res);
assert.equal('bar', body.foo);
});

it('should work against an HTTP endpoint with multiple SOCKS proxies', async () => {
const secondSocksServer = socks.createServer(function (
// @ts-expect-error no types for `socksv5`
_info,
// @ts-expect-error no types for `socksv5`
accept
) {
accept();
});
await listen(secondSocksServer);
const port = secondSocksServer.address().port;
const secondSocksServerUrl = new URL(`socks://127.0.0.1:${port}`);
secondSocksServer.useAuth(socks.auth.None());

httpServer.once('request', function (req, res) {
assert.equal('/foo', req.url);
res.statusCode = 404;
res.end(JSON.stringify(req.headers));
});

const res = await req(new URL('/foo', httpServerUrl), {
agent: new SocksProxyAgent([
socksServerUrl,
secondSocksServerUrl,
]),
headers: { foo: 'bar' },
});
assert.equal(404, res.statusCode);

const body = await json(res);
assert.equal('bar', body.foo);

secondSocksServer.close();
});
});

describe('"https" module', () => {
Expand Down

0 comments on commit d38eddb

Please sign in to comment.