Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): add config.auth param to provide basic auth credentials #55

Merged
merged 1 commit into from
May 1, 2023
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
7 changes: 7 additions & 0 deletions docs/content/3.api/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ Where to emit lighthouse reports and the runtime client.

Display the loggers' debug messages.

### auth

- **Type:** `false|{ username: string, password: string }`
- **Default:** `false`

Optional basic auth credentials

### hooks

- **Type:** `NestedHooks<UnlighthouseHooks>`
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const validateHost = async (resolvedConfig: ResolvedUserConfig) => {
if (resolvedConfig.site) {
// test HTTP response from site
logger.debug(`Testing Site \`${resolvedConfig.site}\` is valid.`)
const { valid, response, error, redirected, redirectUrl } = await fetchUrlRaw(resolvedConfig.site)
const { valid, response, error, redirected, redirectUrl } = await fetchUrlRaw(resolvedConfig.site, resolvedConfig)
if (!valid) {
// something is wrong with the site, bail
if (response?.status)
Expand Down
2 changes: 2 additions & 0 deletions packages/core/lighthouse.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ declare module 'lighthouse' {
precomputedLanternData?: PrecomputedLanternData | null
/** The budget.json object for LightWallet. */
budgets?: Array<Budget> | null
/** Optional extra headers */
extraHeaders?: { [key: string]: string }
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/puppeteer/tasks/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const extractHtmlPayload: (page: Page, route: string) => Promise<{ succes

// if we don't need to execute any javascript we can do a less expensive fetch of the URL
if (resolvedConfig.scanner.skipJavascript) {
const { valid, response, redirected, redirectUrl } = await fetchUrlRaw(route)
const { valid, response, redirected, redirectUrl } = await fetchUrlRaw(route, resolvedConfig)
if (!valid || !response)
return { success: false, message: `Invalid response from URL ${route} code: ${response?.status || '404'}.` }

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/puppeteer/tasks/lighthouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export const runLighthouseTask: PuppeteerTask = async (props) => {
// ignore csp errors
await page.setBypassCSP(true)

if (resolvedConfig.auth) {
page.authenticate(resolvedConfig.auth)
}

// Wait for Lighthouse to open url, then allow hook to run
browser.on('targetchanged', async (target) => {
const page = await target.page()
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export const resolveUserConfig: (userConfig: UserConfig) => Promise<ResolvedUser
}
}

if (config.auth) {
config.lighthouseOptions.extraHeaders = config.lighthouseOptions.extraHeaders || {}
if (!config.lighthouseOptions.extraHeaders['Authorization']) {
const credentials = `${config.auth.username}:${config.auth.password}`
config.lighthouseOptions.extraHeaders["Authorization"] = 'Basic ' + Buffer.from(credentials).toString("base64")
}
}

if (config.client?.columns) {
// filter out any columns for categories we're not showing
config.client.columns = pick(config.client.columns, ['overview', ...config.lighthouseOptions.onlyCategories as UnlighthouseTabs[]])
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ export interface ResolvedUserConfig {
* @default true
*/
cache: boolean
/**
* Optional basic auth credentials
*
* @default false
*/
auth: false | { username: string, password: string }
/**
* Load the configuration from a custom config file.
* By default, it attempts to load configuration from `unlighthouse.config.ts`.
Expand Down
21 changes: 16 additions & 5 deletions packages/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { ensureDirSync } from 'fs-extra'
import sanitize from 'sanitize-filename'
import slugify from 'slugify'
import { hasProtocol, joinURL, withLeadingSlash, withTrailingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
import type { AxiosResponse } from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import type { NormalisedRoute, UnlighthouseRouteReport } from './types'
import type { NormalisedRoute, ResolvedUserConfig, UnlighthouseRouteReport } from './types'
import { useUnlighthouse } from './unlighthouse'

export const ReportArtifacts = {
Expand Down Expand Up @@ -118,16 +118,27 @@ export const formatBytes = (bytes: number, decimals = 2) => {
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
}

export async function fetchUrlRaw(url: string): Promise<{ error?: any; redirected?: boolean; redirectUrl?: string; valid: boolean; response?: AxiosResponse }> {
export async function fetchUrlRaw(url: string, resolvedConfig: ResolvedUserConfig): Promise<{ error?: any; redirected?: boolean; redirectUrl?: string; valid: boolean; response?: AxiosResponse }> {
const axiosOptions: AxiosRequestConfig = {}
if (resolvedConfig.auth) {
axiosOptions.auth = resolvedConfig.auth
}

try {
const response = await axios.get(url, {
// allow all SSL's
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
...axiosOptions,
})
const redirected = response.request.res.responseUrl && response.request.res.responseUrl !== url
const redirectUrl = response.request.res.responseUrl
let responseUrl = response.request.res.responseUrl
if (responseUrl && axiosOptions.auth) {
// remove auth credentials from url (e.g. https://user:passwd@domain.de)
responseUrl = responseUrl.replace(/(?<=https?:\/\/)(.+@)/g, '')
}
const redirected = responseUrl && responseUrl !== url;
const redirectUrl = responseUrl;
if (response.status < 200 || (response.status >= 300 && !redirected)) {
return {
valid: false,
Expand Down