diff --git a/.changeset/chatty-insects-push.md b/.changeset/chatty-insects-push.md new file mode 100644 index 0000000000..5fc447a3ff --- /dev/null +++ b/.changeset/chatty-insects-push.md @@ -0,0 +1,6 @@ +--- +"@pnpm/network.auth-header": minor +"pnpm": minor +--- + +Add support for basic authorization header [#7371](https://github.com/pnpm/pnpm/issues/7371). diff --git a/network/auth-header/src/helpers/removePort.ts b/network/auth-header/src/helpers/removePort.ts index 16d6c76b89..d5945b646d 100644 --- a/network/auth-header/src/helpers/removePort.ts +++ b/network/auth-header/src/helpers/removePort.ts @@ -1,7 +1,6 @@ -import { URL } from 'url' +import { type URL } from 'url' -export function removePort (originalUrl: string) { - const urlObj = new URL(originalUrl) +export function removePort (urlObj: URL) { if (urlObj.port === '') return urlObj.href urlObj.port = '' return urlObj.toString() diff --git a/network/auth-header/src/index.ts b/network/auth-header/src/index.ts index 240c032cfc..3ed56239f4 100644 --- a/network/auth-header/src/index.ts +++ b/network/auth-header/src/index.ts @@ -12,7 +12,7 @@ export function createGetAuthHeaderByURI ( allSettings: opts.allSettings, userSettings: opts.userSettings ?? {}, }) - if (Object.keys(authHeaders).length === 0) return () => undefined + if (Object.keys(authHeaders).length === 0) return (uri: string) => basicAuth(new URL(uri)) return getAuthHeaderByURI.bind(null, authHeaders, getMaxParts(Object.keys(authHeaders))) } @@ -27,15 +27,24 @@ function getAuthHeaderByURI (authHeaders: Record, maxParts: numb if (!uri.endsWith('/')) { uri += '/' } + const parsedUri = new URL(uri) + const basic = basicAuth(parsedUri) + if (basic) return basic const nerfed = nerfDart(uri) const parts = nerfed.split('/') for (let i = Math.min(parts.length, maxParts) - 1; i >= 3; i--) { const key = `${parts.slice(0, i).join('/')}/` if (authHeaders[key]) return authHeaders[key] } - const urlWithoutPort = removePort(uri) + const urlWithoutPort = removePort(parsedUri) if (urlWithoutPort !== uri) { return getAuthHeaderByURI(authHeaders, maxParts, urlWithoutPort) } return undefined } + +function basicAuth (uri: URL): string | undefined { + if (!uri.username && !uri.password) return undefined + const auth64 = btoa(`${uri.username}:${uri.password}`) + return `Basic ${auth64}` +} diff --git a/network/auth-header/test/getAuthHeaderByURI.ts b/network/auth-header/test/getAuthHeaderByURI.ts index 706bc7263e..fa5f7671b3 100644 --- a/network/auth-header/test/getAuthHeaderByURI.ts +++ b/network/auth-header/test/getAuthHeaderByURI.ts @@ -21,6 +21,27 @@ test('getAuthHeaderByURI()', () => { expect(getAuthHeaderByURI('https://reg.gg:8888/foo/-/foo-1.0.0.tgz')).toBe('Bearer 0000') }) +test('getAuthHeaderByURI() basic auth without settings', () => { + const getAuthHeaderByURI = createGetAuthHeaderByURI({ + allSettings: {}, + }) + expect(getAuthHeaderByURI('https://user:secret@reg.io/')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:@reg.io/')).toBe('Basic ' + btoa('user:')) + expect(getAuthHeaderByURI('https://:secret@reg.io/')).toBe('Basic ' + btoa(':secret')) + expect(getAuthHeaderByURI('https://user@reg.io/')).toBe('Basic ' + btoa('user:')) +}) + +test('getAuthHeaderByURI() basic auth with settings', () => { + const getAuthHeaderByURI = createGetAuthHeaderByURI(opts) + expect(getAuthHeaderByURI('https://user:secret@reg.com/')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:secret@reg.com/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:secret@reg.com:8080/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:secret@reg.io/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:secret@reg.co/tarballs/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:secret@reg.gg:8888/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret')) + expect(getAuthHeaderByURI('https://user:secret@reg.gg:8888/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret')) +}) + test('getAuthHeaderByURI() https port 443 checks', () => { const getAuthHeaderByURI = createGetAuthHeaderByURI(opts) expect(getAuthHeaderByURI('https://custom.domain.com:443/artifactory/api/npm/npm-virtual/')).toBe('Bearer xyz') @@ -60,4 +81,4 @@ test('getAuthHeaderByURI() when the registry has pathnames', () => { expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123') expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123') expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123') -}) \ No newline at end of file +}) diff --git a/network/auth-header/test/removePort.test.ts b/network/auth-header/test/removePort.test.ts index 2ffad678be..63c9bc96ef 100644 --- a/network/auth-header/test/removePort.test.ts +++ b/network/auth-header/test/removePort.test.ts @@ -3,10 +3,10 @@ import { removePort } from '../src/helpers/removePort' describe('removePort()', () => { it('does not mutate the url if no port is found', () => { const urlString = 'https://custom.domain.com/npm/-/foo-1.0.0.tgz' - expect(removePort(urlString)).toEqual(urlString) + expect(removePort(new URL(urlString))).toEqual(urlString) const urlStringWithTrailingSlash = 'https://custom.domain.com/npm/' - expect(removePort(urlStringWithTrailingSlash)).toEqual( + expect(removePort(new URL(urlStringWithTrailingSlash))).toEqual( urlStringWithTrailingSlash ) }) @@ -16,7 +16,7 @@ describe('removePort()', () => { const protocols = ['http', 'https', 'ws', 'wss'] const getUrl = (port: number, protocol: string) => - `${protocol}://custom.domain.com:${port}/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz` + new URL(`${protocol}://custom.domain.com:${port}/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz`) const expectedOutput = (protocol: string) => `${protocol}://custom.domain.com/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz` @@ -39,7 +39,7 @@ describe('removePort()', () => { ]) const getUrl = (port: number, protocol: string) => - `${protocol}://custom.domain.com:${port}/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz` + new URL(`${protocol}://custom.domain.com:${port}/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz`) const expectedOutput = (protocol: string) => `${protocol}://custom.domain.com/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz` @@ -67,7 +67,7 @@ describe('removePort()', () => { ]) const getUrl = (port: number, protocol: string) => - `${protocol}://custom.domain.com:${port}/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz` + new URL(`${protocol}://custom.domain.com:${port}/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz`) const expectedOutput = (protocol: string) => `${protocol}://custom.domain.com/artifactory/api/npm/npm-virtual/-/foo-1.0.0.tgz` mismatchProtocolPorts.forEach((value: number, protocol) => {