Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions lib/dispatcher/socks5-proxy-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const debug = debuglog('undici:socks5-proxy')
const kProxyUrl = Symbol('proxy url')
const kProxyHeaders = Symbol('proxy headers')
const kProxyAuth = Symbol('proxy auth')
const kPool = Symbol('pool')
const kPools = Symbol('pools')
const kConnector = Symbol('connector')

// Static flag to ensure warning is only emitted once per process
Expand Down Expand Up @@ -65,8 +65,8 @@ class Socks5ProxyAgent extends DispatcherBase {
servername: options.proxyTls?.servername || url.hostname
})

// Pool for the actual HTTP connections (with SOCKS5 tunnel connect function)
this[kPool] = null
// Pools for the actual HTTP connections (with SOCKS5 tunnel connect function), keyed by origin
this[kPools] = new Map()
}

/**
Expand Down Expand Up @@ -183,9 +183,11 @@ class Socks5ProxyAgent extends DispatcherBase {
debug('dispatching request to', origin, 'via SOCKS5')

try {
// Create Pool with custom connect function if we don't have one yet
if (!this[kPool] || this[kPool].destroyed || this[kPool].closed) {
this[kPool] = new Pool(origin, {
const originKey = String(origin)
let pool = this[kPools].get(originKey)
// Create a Pool per origin so requests are not routed to the wrong host
if (!pool || pool.destroyed || pool.closed) {
pool = new Pool(origin, {
pipelining: opts.pipelining,
connections: opts.connections,
connect: async (connectOpts, callback) => {
Expand Down Expand Up @@ -225,10 +227,11 @@ class Socks5ProxyAgent extends DispatcherBase {
}
}
})
this[kPools].set(originKey, pool)
}

// Dispatch the request through the pool
return this[kPool][kDispatch](opts, handler)
// Dispatch the request through the per-origin pool
return pool[kDispatch](opts, handler)
} catch (err) {
debug('dispatch error:', err)
if (typeof handler.onError === 'function') {
Expand All @@ -240,15 +243,21 @@ class Socks5ProxyAgent extends DispatcherBase {
}

async [kClose] () {
if (this[kPool]) {
await this[kPool].close()
const closePromises = []
for (const pool of this[kPools].values()) {
closePromises.push(pool.close())
}
this[kPools].clear()
await Promise.all(closePromises)
}

async [kDestroy] (err) {
if (this[kPool]) {
await this[kPool].destroy(err)
const destroyPromises = []
for (const pool of this[kPools].values()) {
destroyPromises.push(pool.destroy(err))
}
this[kPools].clear()
await Promise.all(destroyPromises)
}
}

Expand Down
42 changes: 42 additions & 0 deletions test/socks5-proxy-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,48 @@ test('Socks5ProxyAgent - multiple requests through same proxy', async (t) => {
await p.completed
})

test('Socks5ProxyAgent - requests to different origins are routed correctly', async (t) => {
const p = tspl(t, { plan: 4 })

// Create two distinct target servers
const serverA = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ server: 'A', path: req.url }))
})
const serverB = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ server: 'B', path: req.url }))
})

await new Promise((resolve) => serverA.listen(0, '127.0.0.1', resolve))
await new Promise((resolve) => serverB.listen(0, '127.0.0.1', resolve))
const portA = serverA.address().port
const portB = serverB.address().port

const socksServer = new TestSocks5Server()
const socksAddress = await socksServer.listen()

try {
const proxyWrapper = new Socks5ProxyAgent(`socks5://127.0.0.1:${socksAddress.port}`)

// First request goes to server A — establishes a pool
const respA = await request(`http://127.0.0.1:${portA}/a`, { dispatcher: proxyWrapper })
p.equal(respA.statusCode, 200)
p.deepEqual(await respA.body.json(), { server: 'A', path: '/a' })

// Second request goes to server B — must NOT reuse the pool from origin A
const respB = await request(`http://127.0.0.1:${portB}/b`, { dispatcher: proxyWrapper })
p.equal(respB.statusCode, 200)
p.deepEqual(await respB.body.json(), { server: 'B', path: '/b' }, 'request to origin B must reach server B, not server A')
} finally {
await socksServer.close()
serverA.close()
serverB.close()
}

await p.completed
})

test('Socks5ProxyAgent - connection failure', async (t) => {
const p = tspl(t, { plan: 1 })

Expand Down
Loading