Skip to content

Commit d38915d

Browse files
mccmrunalMrunal chaudharichristian-bromann
authored
feat: support custom CDN URL with Basic Auth credentials (#282) (#534)
* feat: support custom CDN URL with Basic Auth credentials (#282) * Update src/utils.ts --------- Co-authored-by: Mrunal chaudhari <mchaudhari@tjc-group.com> Co-authored-by: Christian Bromann <git@bromann.dev>
1 parent c00691b commit d38915d

File tree

3 files changed

+70
-6
lines changed

3 files changed

+70
-6
lines changed

src/install.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { HttpProxyAgent } from 'http-proxy-agent'
1111

1212
import findEdgePath from './finder.js'
1313
import { TAGGED_VERSIONS, EDGE_PRODUCTS_API, EDGEDRIVER_BUCKET, TAGGED_VERSION_URL, LATEST_RELEASE_URL, DOWNLOAD_URL, BINARY_FILE, log } from './constants.js'
14-
import { hasAccess, getNameByArchitecture, sleep } from './utils.js'
14+
import { hasAccess, getNameByArchitecture, sleep, extractBasicAuthFromUrl } from './utils.js'
1515

1616
interface ProductAPIResponse {
1717
Product: string
@@ -67,9 +67,14 @@ export async function download (
6767

6868
async function downloadDriver(version: string) {
6969
try {
70-
const downloadUrl = format(DOWNLOAD_URL, version, getNameByArchitecture())
70+
const rawDownloadUrl = format(DOWNLOAD_URL, version, getNameByArchitecture())
71+
const { url: downloadUrl, authHeader } = extractBasicAuthFromUrl(rawDownloadUrl)
7172
log.info(`Downloading Edgedriver from ${downloadUrl}`)
72-
const res = await fetch(downloadUrl, fetchOpts)
73+
const opts: NodeRequestInit = { ...fetchOpts }
74+
if (authHeader) {
75+
opts.headers = { ...opts.headers, Authorization: authHeader }
76+
}
77+
const res = await fetch(downloadUrl, opts)
7378

7479
if (!res.body || !res.ok || res.status !== 200) {
7580
throw new Error(`Failed to download binary from ${downloadUrl} (statusCode ${res.status})`)
@@ -112,9 +117,14 @@ async function downloadDriver(version: string) {
112117
log.info(`Downloading alternative Edgedriver version from ${alternativeDownloadUrl}`)
113118
const versionResponse = await fetch(alternativeDownloadUrl, fetchOpts)
114119
const alternativeVersion = sanitizeVersion(await versionResponse.text())
115-
const downloadUrl = format(DOWNLOAD_URL, alternativeVersion, getNameByArchitecture())
120+
const rawDownloadUrl = format(DOWNLOAD_URL, alternativeVersion, getNameByArchitecture())
121+
const { url: downloadUrl, authHeader } = extractBasicAuthFromUrl(rawDownloadUrl)
116122
log.info(`Downloading Edgedriver from ${downloadUrl}`)
117-
const res = await fetch(downloadUrl, fetchOpts)
123+
const opts: NodeRequestInit = { ...fetchOpts }
124+
if (authHeader) {
125+
opts.headers = { ...opts.headers, Authorization: authHeader }
126+
}
127+
const res = await fetch(downloadUrl, opts)
118128
if (!res.body || !res.ok || res.status !== 200) {
119129
throw new Error(`Failed to download binary from ${downloadUrl} (statusCode ${res.status})`)
120130
}

src/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,32 @@ export async function hasAccess(filePath: string) {
110110
export function sleep (ms = 100) {
111111
return new Promise((resolve) => setTimeout(resolve, ms))
112112
}
113+
114+
export interface BasicAuthResult {
115+
url: string
116+
authHeader?: string
117+
}
118+
119+
/**
120+
* Extract Basic Auth credentials from a URL and return the cleaned URL with auth header.
121+
* This is needed because fetch() doesn't support URLs with embedded credentials.
122+
* @param urlString URL that may contain credentials (e.g., https://user:pass@host/)
123+
* @returns Object with cleaned URL and optional Authorization header
124+
*/
125+
export function extractBasicAuthFromUrl(urlString: string): BasicAuthResult {
126+
try {
127+
const url = new URL(urlString)
128+
if (url.username || url.password) {
129+
const credentials = btoa(`${url.username}:${url.password}`)
130+
url.username = ''
131+
url.password = ''
132+
return {
133+
url: url.toString(),
134+
authHeader: `Basic ${credentials}`
135+
}
136+
}
137+
} catch {
138+
// If URL parsing fails, return original string
139+
}
140+
return { url: urlString }
141+
}

tests/unit.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { vi, test, expect } from 'vitest'
33

44
import * as pkgExports from '../src/index.js'
55
import { fetchVersion } from '../src/install.js'
6-
import { getNameByArchitecture, parseParams } from '../src/utils.js'
6+
import { getNameByArchitecture, parseParams, extractBasicAuthFromUrl } from '../src/utils.js'
77
import { EDGE_PRODUCTS_API } from '../src/constants.js'
88

99
vi.mock('node:os', () => ({
@@ -116,3 +116,28 @@ test('exports', () => {
116116
expect(typeof pkgExports.findEdgePath).toBe('function')
117117
expect(typeof pkgExports.start).toBe('function')
118118
})
119+
120+
test('extractBasicAuthFromUrl with credentials', () => {
121+
const result = extractBasicAuthFromUrl('https://myuser:mypassword@cdn.example.com/path/file.zip')
122+
expect(result.url).toBe('https://cdn.example.com/path/file.zip')
123+
expect(result.authHeader).toBe('Basic ' + Buffer.from('myuser:mypassword').toString('base64'))
124+
})
125+
126+
test('extractBasicAuthFromUrl without credentials', () => {
127+
const result = extractBasicAuthFromUrl('https://cdn.example.com/path/file.zip')
128+
expect(result.url).toBe('https://cdn.example.com/path/file.zip')
129+
expect(result.authHeader).toBeUndefined()
130+
})
131+
132+
test('extractBasicAuthFromUrl with only username', () => {
133+
const result = extractBasicAuthFromUrl('https://myuser@cdn.example.com/path/file.zip')
134+
expect(result.url).toBe('https://cdn.example.com/path/file.zip')
135+
expect(result.authHeader).toBe('Basic ' + Buffer.from('myuser:').toString('base64'))
136+
})
137+
138+
test('extractBasicAuthFromUrl with invalid URL returns original', () => {
139+
const result = extractBasicAuthFromUrl('not-a-valid-url')
140+
expect(result.url).toBe('not-a-valid-url')
141+
expect(result.authHeader).toBeUndefined()
142+
})
143+

0 commit comments

Comments
 (0)