diff --git a/.changeset/silver-pumas-drum.md b/.changeset/silver-pumas-drum.md new file mode 100644 index 0000000..e19481e --- /dev/null +++ b/.changeset/silver-pumas-drum.md @@ -0,0 +1,5 @@ +--- +"socks-proxy-agent": patch +--- + +Pass `socket_options` to `SocksClient` diff --git a/packages/socks-proxy-agent/src/index.ts b/packages/socks-proxy-agent/src/index.ts index 0735987..c6b84db 100644 --- a/packages/socks-proxy-agent/src/index.ts +++ b/packages/socks-proxy-agent/src/index.ts @@ -71,11 +71,15 @@ function parseSocksURL(url: URL): { lookup: boolean; proxy: SocksProxy } { return { lookup, proxy }; } +type SocksSocketOptions = Omit; + export type SocksProxyAgentOptions = Omit< SocksProxy, // These come from the parsed URL 'ipaddress' | 'host' | 'port' | 'type' | 'userId' | 'password' -> & +> & { + socketOptions?: SocksSocketOptions; +} & http.AgentOptions; export class SocksProxyAgent extends Agent { @@ -90,6 +94,7 @@ export class SocksProxyAgent extends Agent { readonly shouldLookup: boolean; readonly proxy: SocksProxy; timeout: number | null; + socketOptions: SocksSocketOptions | null; constructor(uri: string | URL, opts?: SocksProxyAgentOptions) { super(opts); @@ -100,6 +105,7 @@ export class SocksProxyAgent extends Agent { this.shouldLookup = lookup; this.proxy = proxy; this.timeout = opts?.timeout ?? null; + this.socketOptions = opts?.socketOptions ?? null; } /** @@ -141,6 +147,9 @@ export class SocksProxyAgent extends Agent { }, command: 'connect', timeout: timeout ?? undefined, + // @ts-expect-error the type supplied by socks for socket_options is wider + // than necessary since socks will always override the host and port + socket_options: this.socketOptions ?? undefined, }; const cleanup = (tlsSocket?: tls.TLSSocket) => { diff --git a/packages/socks-proxy-agent/test/test.ts b/packages/socks-proxy-agent/test/test.ts index 737e883..9192862 100644 --- a/packages/socks-proxy-agent/test/test.ts +++ b/packages/socks-proxy-agent/test/test.ts @@ -21,19 +21,26 @@ describe('SocksProxyAgent', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let socksServer: any; let socksServerUrl: URL; + let socksServerHost: string | null = null; - beforeAll(async () => { + beforeEach(async () => { // setup SOCKS proxy server // @ts-expect-error no types for `socksv5` socksServer = socks.createServer(function (_info, accept) { accept(); }); - await listen(socksServer); - const port = socksServer.address().port; - socksServerUrl = new URL(`socks://127.0.0.1:${port}`); + await listen(socksServer, 0, socksServerHost ?? '127.0.0.1'); + const { port, family, address } = socksServer.address(); + socksServerUrl = new URL( + `socks://${family === 'IPv6' ? 'localhost' : address}:${port}` + ); socksServer.useAuth(socks.auth.None()); }); + afterEach(() => { + socksServer.close(); + }); + beforeAll(async () => { // setup target HTTP server httpServer = http.createServer(); @@ -55,7 +62,6 @@ describe('SocksProxyAgent', () => { }); afterAll(() => { - socksServer.close(); httpServer.close(); httpsServer.close(); }); @@ -90,6 +96,37 @@ describe('SocksProxyAgent', () => { }); }); + describe('ipv6 host', () => { + beforeAll(() => { + socksServerHost = '::1'; + }); + afterAll(() => { + socksServerHost = null; + }); + + it('should connect over ipv6 socket', async () => { + httpServer.once('request', (req, res) => res.end()); + + const res = await req(new URL('/foo', httpServerUrl), { + agent: new SocksProxyAgent(socksServerUrl, { socketOptions: { family: 6 } }), + }); + assert(res); + }); + + it('should refuse connection over ipv4 socket', async () => { + let err: Error | undefined; + try { + await req(new URL('/foo', httpServerUrl), { + agent: new SocksProxyAgent(socksServerUrl, { socketOptions: { family: 4 } }), + }); + } catch (_err) { + err = _err as Error; + } + assert(err); + assert.equal(err.message, `connect ECONNREFUSED 127.0.0.1:${socksServerUrl.port}`); + }); + }); + describe('"http" module', () => { it('should work against an HTTP endpoint', async () => { httpServer.once('request', function (req, res) {