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
30 changes: 13 additions & 17 deletions lib/dispatcher/socks5-proxy-agent.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const net = require('node:net')
const { URL } = require('node:url')

let tls // include tls conditionally since it is not always available
Expand All @@ -17,6 +16,7 @@ const debug = debuglog('undici:socks5-proxy')
const kProxyUrl = Symbol('proxy url')
const kProxyHeaders = Symbol('proxy headers')
const kProxyAuth = Symbol('proxy auth')
const kProxyProtocol = Symbol('proxy protocol')
const kPools = Symbol('pools')
const kConnector = Symbol('connector')

Expand Down Expand Up @@ -52,6 +52,7 @@ class Socks5ProxyAgent extends DispatcherBase {

this[kProxyUrl] = url
this[kProxyHeaders] = options.headers || {}
this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:'

// Extract auth from URL or options
this[kProxyAuth] = {
Expand Down Expand Up @@ -81,25 +82,20 @@ class Socks5ProxyAgent extends DispatcherBase {
// Connect to the SOCKS5 proxy
const socketReady = Promise.withResolvers()

const onSocketConnect = () => {
socket.removeListener('error', onSocketError)
socketReady.resolve(socket)
}

const onSocketError = (err) => {
socket.removeListener('connect', onSocketConnect)
socketReady.reject(err)
}

const socket = net.connect({
this[kConnector]({
hostname: proxyHost,
host: proxyHost,
port: proxyPort
port: proxyPort,
protocol: this[kProxyProtocol]
}, (err, socket) => {
if (err) {
socketReady.reject(err)
} else {
socketReady.resolve(socket)
}
})

socket.once('connect', onSocketConnect)
socket.once('error', onSocketError)

await socketReady.promise
const socket = await socketReady.promise

// Create SOCKS5 client
const socks5Client = new Socks5Client(socket, this[kProxyAuth])
Expand Down
50 changes: 50 additions & 0 deletions test/socks5-proxy-agent.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const net = require('node:net')
const { tspl } = require('@matteo.collina/tspl')
const { test } = require('node:test')
const { request } = require('..')
Expand Down Expand Up @@ -34,6 +35,55 @@ test('Socks5ProxyAgent - constructor validation', async (t) => {
await p.completed
})

test('Socks5ProxyAgent - uses custom connector for proxy connection', async (t) => {
const p = tspl(t, { plan: 3 })

const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ ok: true }))
})

await new Promise((resolve) => {
server.listen(0, resolve)
})
const serverPort = server.address().port

const socksServer = new TestSocks5Server()
const socksAddress = await socksServer.listen()
let connectorCalled = false

const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`, {
connect (opts, callback) {
connectorCalled = true
const socket = net.connect({
host: opts.hostname,
port: opts.port
})
socket.once('connect', () => callback(null, socket))
socket.once('error', callback)
return socket
}
})

const response = await request(`http://localhost:${serverPort}/test`, {
dispatcher: proxyWrapper
})

p.equal(response.statusCode, 200, 'should get 200 status code')
p.deepEqual(await response.body.json(), { ok: true }, 'should get correct response body')
p.equal(connectorCalled, true, 'should use custom connector for proxy connection')

t.after(async () => {
if (proxyWrapper) {
await proxyWrapper.close()
}
await socksServer.close()
server.close()
})

await p.completed
})

test('Socks5ProxyAgent - basic HTTP connection', async (t) => {
const p = tspl(t, { plan: 2 })

Expand Down
Loading