From d65d0caf627e8d1f5367db34d7d9b55d332c1efb Mon Sep 17 00:00:00 2001 From: Szymon Marczak <36894700+szmarczak@users.noreply.github.com> Date: Wed, 4 Aug 2021 23:20:59 +0200 Subject: [PATCH] Fix `username` and `password` encoding in URL This is a Node.js bug, but it's better to fix this in Got. In the future we may use a different HTTP client, which doesn't support these properties in URL. Fixes #1169 Fixes #1317 --- source/core/index.ts | 7 ++++++- source/core/options.ts | 23 +++++++++++------------ test/headers.ts | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/source/core/index.ts b/source/core/index.ts index 806748543..601aa3486 100644 --- a/source/core/index.ts +++ b/source/core/index.ts @@ -1042,7 +1042,7 @@ export default class Request extends Duplex implements RequestEvents { private async _makeRequest(): Promise { const {options} = this; - const {headers} = options; + const {headers, username, password} = options; const cookieJar = options.cookieJar as PromiseCookieJar | undefined; for (const key in headers) { @@ -1058,6 +1058,11 @@ export default class Request extends Duplex implements RequestEvents { headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate'; } + if (username || password) { + const credentials = Buffer.from(`${username}:${password}`).toString('base64'); + headers.authorization = `Basic ${credentials}`; + } + // Set cookies if (cookieJar) { const cookieString: string = await cookieJar.getCookieString(options.url!.toString()); diff --git a/source/core/options.ts b/source/core/options.ts index a10ec22e5..24c5c9a9e 100644 --- a/source/core/options.ts +++ b/source/core/options.ts @@ -1608,33 +1608,30 @@ export default class Options { get username(): string { const url = this._internals.url as URL; - if (url) { - return url.username; - } + const value = url ? url.username : this._internals.username; - return this._internals.username; + return decodeURIComponent(value); } set username(value: string) { assert.string(value); const url = this._internals.url as URL; + const fixedValue = encodeURIComponent(value); if (url) { - url.username = value; + url.username = fixedValue; } else { - this._internals.username = value; + this._internals.username = fixedValue; } } get password(): string { const url = this._internals.url as URL; - if (url) { - return url.password; - } + const value = url ? url.password : this._internals.password; - return this._internals.password; + return decodeURIComponent(value); } set password(value: string) { @@ -1642,10 +1639,12 @@ export default class Options { const url = this._internals.url as URL; + const fixedValue = encodeURIComponent(value); + if (url) { - url.password = value; + url.password = fixedValue; } else { - this._internals.password = value; + this._internals.password = fixedValue; } } diff --git a/test/headers.ts b/test/headers.ts index 3f2f3d5b1..d56ac605e 100644 --- a/test/headers.ts +++ b/test/headers.ts @@ -260,3 +260,19 @@ test('strip port in host header if implicit standard port & protocol (HTTPS)', a const body = await got('https://httpbin.org/headers').json<{headers: Headers}>(); t.is(body.headers.Host, 'httpbin.org'); }); + +test('correctly encodes authorization header', withServer, async (t, server, got) => { + server.get('/', echoHeaders); + + const {authorization} = await got('', {username: 'test@'}).json(); + + t.is(authorization, `Basic ${Buffer.from('test@:').toString('base64')}`); +}); + +test('url passes if credentials contain special characters', withServer, async (t, server, got) => { + server.get('/', echoHeaders); + + const {authorization} = await got('', {password: 't$es%t'}).json(); + + t.is(authorization, `Basic ${Buffer.from(':t$es%t').toString('base64')}`); +});