Skip to content

Commit ec60a7c

Browse files
authored
feat: support username-only proxy authentication in ProxyAgent (#4935)
When a proxy URL contains a username but no password (e.g., http://user:@host or http://user@host), ProxyAgent now correctly generates a Basic auth header with just the username.
1 parent c7a2901 commit ec60a7c

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

lib/dispatcher/proxy-agent.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class ProxyAgent extends DispatcherBase {
126126
this[kProxyHeaders]['proxy-authorization'] = opts.token
127127
} else if (username && password) {
128128
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
129+
} else if (username) {
130+
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:`).toString('base64')}`
129131
}
130132

131133
const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls })

test/proxy-agent.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,88 @@ test('use proxy-agent to connect through proxy with basic auth in URL', async (t
422422
proxyAgent.close()
423423
})
424424

425+
test('use proxy-agent to connect through proxy with username-only auth in URL', async (t) => {
426+
t = tspl(t, { plan: 6 })
427+
const server = await buildServer()
428+
const proxy = await buildProxy()
429+
430+
const serverUrl = `http://localhost:${server.address().port}`
431+
const proxyUrl = new URL(`http://user:@localhost:${proxy.address().port}`)
432+
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
433+
const parsedOrigin = new URL(serverUrl)
434+
435+
proxy.authenticate = function (req) {
436+
t.ok(true, 'authentication should be called')
437+
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:').toString('base64')}`
438+
}
439+
proxy.on('connect', () => {
440+
t.fail('proxy tunnel should not be established')
441+
})
442+
443+
server.on('request', (req, res) => {
444+
t.strictEqual(req.url, '/hello?foo=bar')
445+
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
446+
res.setHeader('content-type', 'application/json')
447+
res.end(JSON.stringify({ hello: 'world' }))
448+
})
449+
450+
const {
451+
statusCode,
452+
headers,
453+
body
454+
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
455+
const json = await body.json()
456+
457+
t.strictEqual(statusCode, 200)
458+
t.deepStrictEqual(json, { hello: 'world' })
459+
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
460+
461+
server.close()
462+
proxy.close()
463+
proxyAgent.close()
464+
})
465+
466+
test('use proxy-agent to connect through proxy with username-only auth in URL without colon', async (t) => {
467+
t = tspl(t, { plan: 6 })
468+
const server = await buildServer()
469+
const proxy = await buildProxy()
470+
471+
const serverUrl = `http://localhost:${server.address().port}`
472+
const proxyUrl = new URL(`http://user@localhost:${proxy.address().port}`)
473+
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
474+
const parsedOrigin = new URL(serverUrl)
475+
476+
proxy.authenticate = function (req) {
477+
t.ok(true, 'authentication should be called')
478+
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:').toString('base64')}`
479+
}
480+
proxy.on('connect', () => {
481+
t.fail('proxy tunnel should not be established')
482+
})
483+
484+
server.on('request', (req, res) => {
485+
t.strictEqual(req.url, '/hello?foo=bar')
486+
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
487+
res.setHeader('content-type', 'application/json')
488+
res.end(JSON.stringify({ hello: 'world' }))
489+
})
490+
491+
const {
492+
statusCode,
493+
headers,
494+
body
495+
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
496+
const json = await body.json()
497+
498+
t.strictEqual(statusCode, 200)
499+
t.deepStrictEqual(json, { hello: 'world' })
500+
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
501+
502+
server.close()
503+
proxy.close()
504+
proxyAgent.close()
505+
})
506+
425507
test('use proxy-agent to connect through proxy with basic auth in URL with tunneling enabled', async (t) => {
426508
t = tspl(t, { plan: 7 })
427509
const server = await buildServer()
@@ -463,6 +545,47 @@ test('use proxy-agent to connect through proxy with basic auth in URL with tunne
463545
proxyAgent.close()
464546
})
465547

548+
test('use proxy-agent to connect through proxy with username-only auth in URL with tunneling enabled', async (t) => {
549+
t = tspl(t, { plan: 7 })
550+
const server = await buildServer()
551+
const proxy = await buildProxy()
552+
553+
const serverUrl = `http://localhost:${server.address().port}`
554+
const proxyUrl = new URL(`http://user:@localhost:${proxy.address().port}`)
555+
const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
556+
const parsedOrigin = new URL(serverUrl)
557+
558+
proxy.authenticate = function (req) {
559+
t.ok(true, 'authentication should be called')
560+
return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:').toString('base64')}`
561+
}
562+
proxy.on('connect', () => {
563+
t.ok(true, 'proxy should be called')
564+
})
565+
566+
server.on('request', (req, res) => {
567+
t.strictEqual(req.url, '/hello?foo=bar')
568+
t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
569+
res.setHeader('content-type', 'application/json')
570+
res.end(JSON.stringify({ hello: 'world' }))
571+
})
572+
573+
const {
574+
statusCode,
575+
headers,
576+
body
577+
} = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
578+
const json = await body.json()
579+
580+
t.strictEqual(statusCode, 200)
581+
t.deepStrictEqual(json, { hello: 'world' })
582+
t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
583+
584+
server.close()
585+
proxy.close()
586+
proxyAgent.close()
587+
})
588+
466589
test('use proxy-agent with auth', async (t) => {
467590
t = tspl(t, { plan: 6 })
468591
const server = await buildServer()

0 commit comments

Comments
 (0)