diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index f5c2cbc95ef12..68f0c531b3e3c 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1520,9 +1520,12 @@ its geolocation. ### param: BrowserContext.setHTTPCredentials.httpCredentials * since: v1.8 -- `httpCredentials` <[null]|[Object]> +- `httpCredentials` <[null]|[Object]|[Array]<[Object]>> - `username` <[string]> - `password` <[string]> + - `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port). + +Pass an array to use different credentials for different origins. The first entry that matches the request origin is used, and entries with no origin match any request. ## async method: BrowserContext.setOffline * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 47fe73082ada9..bf7a70b9074db 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -718,7 +718,7 @@ An object containing additional HTTP headers to be sent with every request. Defa Whether to emulate network being offline. Defaults to `false`. Learn more about [network emulation](../emulation.md#offline). ## context-option-httpcredentials -- `httpCredentials` <[Object]> +- `httpCredentials` <[Object]|[Array]<[Object]>> * alias: HttpCredentials - `username` <[string]> - `password` <[string]> @@ -728,6 +728,8 @@ Whether to emulate network being offline. Defaults to `false`. Learn more about Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no origin is specified, the username and password are sent to any servers upon unauthorized responses. +Pass an array to use different credentials for different origins. The first entry that matches the request origin is used, and entries with no origin match any request. + ## context-option-colorscheme * langs: js, java - `colorScheme` > diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index eaa024a641d57..baa5f1d759c4a 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10453,13 +10453,28 @@ export interface BrowserContext { /** * @deprecated Browsers may cache credentials after successful authentication. Create a new browser context instead. - * @param httpCredentials + * @param httpCredentials Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ setHTTPCredentials(httpCredentials: null|{ username: string; password: string; - }): Promise; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }|ReadonlyArray<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }>): Promise; /** * @param offline Whether to emulate network being offline for the browser context. @@ -11229,6 +11244,9 @@ export interface Browser { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -11248,7 +11266,25 @@ export interface Browser { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -17270,6 +17306,9 @@ export interface BrowserType { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -17289,7 +17328,25 @@ export interface BrowserType { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * If `true`, Playwright does not pass its own configurations args and only uses the ones from @@ -18885,6 +18942,9 @@ export interface APIRequest { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -18904,7 +18964,25 @@ export interface APIRequest { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -23588,6 +23666,9 @@ export interface Electron { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -23607,7 +23688,25 @@ export interface Electron { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -24225,6 +24324,9 @@ export interface AndroidDevice { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -24244,7 +24346,25 @@ export interface AndroidDevice { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -25457,8 +25577,11 @@ export interface BrowserContextOptions { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ - httpCredentials?: HTTPCredentials; + httpCredentials?: HTTPCredentials|Array; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 3f1ce5e3d63b5..c971f0739048f 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -49,6 +49,7 @@ import { mkdirIfNeeded } from './fileUtils'; import type { EvaluateOptions } from './jsHandle'; import type { BrowserContextOptions, Headers, SetStorageState, StorageState, WaitForEventOptions } from './types'; +import type { HttpCredentials } from '@protocol/structs'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type { URLMatch } from '@isomorphic/urlMatch'; @@ -353,8 +354,8 @@ export class BrowserContext extends ChannelOwner await this._channel.setOffline({ offline }, kNoTimeout); } - async setHTTPCredentials(httpCredentials: { username: string, password: string } | null): Promise { - await this._channel.setHTTPCredentials({ httpCredentials: httpCredentials || undefined }, kNoTimeout); + async setHTTPCredentials(httpCredentials: HttpCredentials | HttpCredentials[] | null): Promise { + await this._channel.setHTTPCredentials({ httpCredentials: toHttpCredentialsProtocol(httpCredentials || undefined) }, kNoTimeout); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any, options?: EvaluateOptions) { @@ -567,12 +568,20 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions contrast: options.contrast === null ? 'no-override' : options.contrast, acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), + httpCredentials: toHttpCredentialsProtocol(options.httpCredentials), }; if (contextParams.recordVideo && contextParams.recordVideo.dir) contextParams.recordVideo.dir = path.resolve(contextParams.recordVideo.dir); return contextParams; } +export function toHttpCredentialsProtocol(credentials?: HttpCredentials | HttpCredentials[]): HttpCredentials[] | undefined { + if (!credentials) + return undefined; + const list = Array.isArray(credentials) ? credentials : [credentials]; + return list.length ? list : undefined; +} + function toAcceptDownloadsProtocol(acceptDownloads?: boolean) { if (acceptDownloads === undefined) return undefined; diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index d62e20c022afd..be860f3deb3ec 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -15,8 +15,8 @@ */ // This file is generated by generate_channels.js, do not edit manually. -import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; -export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; export type TimeoutOptions = { signal: AbortSignal | undefined, timeout: number }; @@ -404,12 +404,7 @@ export type AndroidDeviceLaunchBrowserParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -476,12 +471,7 @@ export type AndroidDeviceLaunchBrowserOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -861,12 +851,7 @@ export type BrowserNewContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -936,12 +921,7 @@ export type BrowserNewContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1014,12 +994,7 @@ export type BrowserNewContextForReuseParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1089,12 +1064,7 @@ export type BrowserNewContextForReuseOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1206,12 +1176,7 @@ export type BrowserContextInitializer = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1481,18 +1446,10 @@ export type BrowserContextSetGeolocationOptions = { }; export type BrowserContextSetGeolocationResult = void; export type BrowserContextSetHTTPCredentialsParams = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsOptions = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsResult = void; export type BrowserContextSetNetworkInterceptionPatternsParams = { @@ -1857,12 +1814,7 @@ export type BrowserTypeLaunchPersistentContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1945,12 +1897,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -2056,11 +2003,7 @@ export type ElectronLaunchParams = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -2099,11 +2042,7 @@ export type ElectronLaunchOptions = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -4651,12 +4590,7 @@ export type PlaywrightNewRequestParams = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], proxy?: { server: string, bypass?: string, @@ -4683,12 +4617,7 @@ export type PlaywrightNewRequestOptions = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], proxy?: { server: string, bypass?: string, diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index d433427c57ba2..927c8a4b9d1ad 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -33,12 +33,13 @@ import type * as childProcess from 'child_process'; import type { BrowserWindow } from 'electron'; import type { Playwright } from './playwright'; -type ElectronOptions = Omit & { +type ElectronOptions = Omit & { env?: NodeJS.ProcessEnv, extraHTTPHeaders?: Headers, recordHar?: BrowserContextOptions['recordHar'], colorScheme?: 'dark' | 'light' | 'no-preference' | null, acceptDownloads?: boolean, + httpCredentials?: BrowserContextOptions['httpCredentials'], timeout?: number, }; diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 73834d56e91b8..54b17863a1745 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -21,7 +21,7 @@ import { inspect } from 'util'; import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { isString } from '@isomorphic/rtti'; -import { toClientCertificatesProtocol } from './browserContext'; +import { toClientCertificatesProtocol, toHttpCredentialsProtocol } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { TargetClosedError, isTargetClosedError } from './errors'; import { RawHeaders } from './network'; @@ -32,6 +32,7 @@ import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; import type { Playwright } from './playwright'; import type { ResourceTiming } from './network'; import type { ClientCertificate, FilePayload, Headers, RemoteAddr, SecurityDetails, SetStorageState, StorageState, TimeoutOptions } from './types'; +import type { HttpCredentials } from '@protocol/structs'; import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray, NameValue } from '@isomorphic/types'; @@ -52,10 +53,11 @@ export type FetchOptions = { maxRetries?: number, }; -export type NewContextOptions = Omit & { +export type NewContextOptions = Omit & { extraHTTPHeaders?: Headers, storageState?: string | SetStorageState, clientCertificates?: ClientCertificate[]; + httpCredentials?: HttpCredentials | HttpCredentials[]; }; type RequestWithBodyOptions = Omit; @@ -80,6 +82,7 @@ export class APIRequest implements api.APIRequest { storageState, tracesDir: this._playwright._defaultLaunchOptions?.tracesDir, // We do not expose tracesDir in the API, so do not allow options to accidentally override it. clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), + httpCredentials: toHttpCredentialsProtocol(options.httpCredentials), }, kNoTimeout)).request); this._contexts.add(context); context._request = this; diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index f340521105e1a..81c562a1be535 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -16,6 +16,7 @@ */ import type { Size } from '@isomorphic/types'; +import type { HttpCredentials } from '@protocol/structs'; import type * as channels from './channels'; export type { HeadersArray, Point, Quad, Rect, Size } from '@isomorphic/types'; @@ -63,9 +64,10 @@ export type ClientCertificate = { passphrase?: string; }; -export type BrowserContextOptions = Omit & { +export type BrowserContextOptions = Omit & { viewport?: Size | null; extraHTTPHeaders?: Headers; + httpCredentials?: HttpCredentials | HttpCredentials[]; logger?: Logger; storageState?: string | SetStorageState; har?: { diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index cf6699c924c6f..95546d70fe53c 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -31,6 +31,7 @@ import type { SdkObject } from '../instrumentation'; import type { InitScript, Page } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { BidiSession } from './bidiConnection'; import type * as channels from '../channels'; @@ -409,7 +410,7 @@ export class BidiBrowserContext extends BrowserContext { }); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { this._options.httpCredentials = httpCredentials; for (const page of this.pages()) await (page.delegate as BidiPage).updateHttpCredentials(); diff --git a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts index c413733c4b067..b78b485331e70 100644 --- a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts +++ b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts @@ -15,6 +15,7 @@ */ import { eventsHelper } from '@utils/eventsHelper'; +import { findMatchingHttpCredentials } from '../browserContext'; import { parseRawCookie } from '../cookieStore'; import * as network from '../network'; import * as bidi from './third_party/bidiProtocol'; @@ -23,6 +24,7 @@ import type { RegisteredListener } from '@utils/eventsHelper'; import type * as frames from '../frames'; import type { Page } from '../page'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { BidiSession } from './bidiConnection'; import type { BidiPage } from './bidiPage'; @@ -35,7 +37,7 @@ export class BidiNetworkManager { private readonly _eventListeners: RegisteredListener[]; private _userRequestInterceptionEnabled: boolean = false; private _protocolRequestInterceptionEnabled: boolean = false; - private _credentials: types.Credentials | undefined; + private _credentials: HttpCredentials[] | undefined; private _attemptedAuthentications = new Set(); private _intercepId: bidi.Network.Intercept | undefined; @@ -197,8 +199,8 @@ export class BidiNetworkManager { private _onAuthRequired(params: bidi.Network.AuthRequiredParameters) { const isBasic = params.response.authChallenges?.some(challenge => challenge.scheme.startsWith('Basic')); - const credentials = this._page.browserContext._options.httpCredentials; - if (isBasic && credentials && (!credentials.origin || (new URL(params.request.url).origin).toLowerCase() === credentials.origin.toLowerCase())) { + const credentials = findMatchingHttpCredentials(this._page.browserContext._options.httpCredentials, params.request.url); + if (isBasic && credentials) { if (this._attemptedAuthentications.has(params.request.request)) { this._session.sendMayFail('network.continueWithAuth', { request: params.request.request, @@ -234,13 +236,13 @@ export class BidiNetworkManager { await this._updateProtocolRequestInterception(); } - async setCredentials(credentials: types.Credentials | undefined) { + async setCredentials(credentials: HttpCredentials[] | undefined) { this._credentials = credentials; await this._updateProtocolRequestInterception(); } async _updateProtocolRequestInterception(initial?: boolean) { - const enabled = this._userRequestInterceptionEnabled || !!this._credentials; + const enabled = this._userRequestInterceptionEnabled || !!this._credentials?.length; if (enabled === this._protocolRequestInterceptionEnabled) return; this._protocolRequestInterceptionEnabled = enabled; diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 04f888bfa5afa..ff4528c3f1c2e 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -45,6 +45,7 @@ import type { ClientCertificatesProxy } from './socksClientCertificatesIntercept import type { SerializedStorage } from '@injected/storageScript'; import type * as types from './types'; import type * as channels from './channels'; +import type { HttpCredentials } from '@protocol/structs'; const BrowserContextEvent = { Console: 'console', @@ -288,7 +289,7 @@ export abstract class BrowserContext extends Sdk protected abstract doClearCookies(): Promise; protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise; protected abstract doClearPermissions(): Promise; - protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise; + protected abstract doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise; protected abstract doAddInitScript(initScript: InitScript): Promise; protected abstract doRemoveInitScripts(initScripts: InitScript[]): Promise; protected abstract doUpdateExtraHTTPHeaders(): Promise; @@ -343,11 +344,11 @@ export abstract class BrowserContext extends Sdk }))); } - setHTTPCredentials(progress: Progress, httpCredentials?: types.Credentials): Promise { + setHTTPCredentials(progress: Progress, httpCredentials?: HttpCredentials[]): Promise { return progress.race(this.innerSetHTTPCredentials(httpCredentials)); } - innerSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + innerSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { return this.doSetHTTPCredentials(httpCredentials); } @@ -479,7 +480,7 @@ export abstract class BrowserContext extends Sdk const proxy = this._options.proxy || this._browser.options.proxy || { username: undefined, password: undefined }; const { username, password } = proxy; if (username) { - this._options.httpCredentials = { username, password: password! }; + this._options.httpCredentials = [{ username, password: password! }]; const token = Buffer.from(`${username}:${password}`).toString('base64'); this._options.extraHTTPHeaders = network.mergeHeaders([ this._options.extraHTTPHeaders, @@ -494,7 +495,7 @@ export abstract class BrowserContext extends Sdk return; const { username, password } = proxy; if (username) - this._options.httpCredentials = { username, password: password || '' }; + this._options.httpCredentials = [{ username, password: password || '' }]; } async addInitScript(progress: Progress, source: string): Promise { @@ -768,6 +769,11 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio verifyGeolocation(options.geolocation); } +export function findMatchingHttpCredentials(credentials: HttpCredentials[] | undefined, url: string): HttpCredentials | undefined { + const origin = new URL(url).origin.toLowerCase(); + return credentials?.find(c => !c.origin || c.origin.toLowerCase() === origin); +} + export function verifyGeolocation(geolocation?: types.Geolocation): asserts geolocation is types.Geolocation { if (!geolocation) return; diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index 07a6414f49295..effc7d3986ccc 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -18,8 +18,8 @@ import type { Progress } from './progress'; -import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; -export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; // ----------- Initializer Traits ----------- export type InitializerTraits = @@ -405,12 +405,7 @@ export type AndroidDeviceLaunchBrowserParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -477,12 +472,7 @@ export type AndroidDeviceLaunchBrowserOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -862,12 +852,7 @@ export type BrowserNewContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -937,12 +922,7 @@ export type BrowserNewContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1015,12 +995,7 @@ export type BrowserNewContextForReuseParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1090,12 +1065,7 @@ export type BrowserNewContextForReuseOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1207,12 +1177,7 @@ export type BrowserContextInitializer = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1482,18 +1447,10 @@ export type BrowserContextSetGeolocationOptions = { }; export type BrowserContextSetGeolocationResult = void; export type BrowserContextSetHTTPCredentialsParams = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsOptions = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsResult = void; export type BrowserContextSetNetworkInterceptionPatternsParams = { @@ -1858,12 +1815,7 @@ export type BrowserTypeLaunchPersistentContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1946,12 +1898,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -2057,11 +2004,7 @@ export type ElectronLaunchParams = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -2100,11 +2043,7 @@ export type ElectronLaunchOptions = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -4652,12 +4591,7 @@ export type PlaywrightNewRequestParams = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], proxy?: { server: string, bypass?: string, @@ -4684,12 +4618,7 @@ export type PlaywrightNewRequestOptions = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], proxy?: { server: string, bypass?: string, diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index ba563a4b56118..db00e29a01059 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -33,6 +33,7 @@ import { CRServiceWorker } from './crServiceWorker'; import type { InitScript } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { CDPSession, CRSession } from './crConnection'; import type { CRDevTools } from './crDevTools'; import type { Protocol } from './protocol'; @@ -509,7 +510,7 @@ export class CRBrowserContext extends BrowserContext { await (sw as CRServiceWorker).updateOffline(); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { this._options.httpCredentials = httpCredentials; for (const page of this.pages()) await (page.delegate as CRPage).updateHttpCredentials(); diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 1c720b5821004..de95ef8dd3d6f 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -18,6 +18,7 @@ import { eventsHelper } from '@utils/eventsHelper'; import { assert } from '@isomorphic/assert'; import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers'; +import { findMatchingHttpCredentials } from '../browserContext'; import { helper } from '../helper'; import * as network from '../network'; import { isProtocolError, isSessionClosedError } from '../protocolError'; @@ -29,6 +30,7 @@ import type * as contexts from '../browserContext'; import type * as frames from '../frames'; import type { Page } from '../page'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { CRPage } from './crPage'; import type { CRServiceWorker } from './crServiceWorker'; @@ -45,7 +47,7 @@ export class CRNetworkManager { private _serviceWorker: CRServiceWorker | null; private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); - private _credentials: {origin?: string, username: string, password: string} | null = null; + private _credentials: HttpCredentials[] | null = null; private _attemptedAuthentications = new Set(); private _userRequestInterceptionEnabled = false; private _protocolRequestInterceptionEnabled = false; @@ -120,7 +122,7 @@ export class CRNetworkManager { })); } - async authenticate(credentials: types.Credentials | null) { + async authenticate(credentials: HttpCredentials[] | null) { this._credentials = credentials; await this._updateProtocolRequestInterception(); } @@ -153,7 +155,7 @@ export class CRNetworkManager { } async _updateProtocolRequestInterception() { - const enabled = this._userRequestInterceptionEnabled || !!this._credentials; + const enabled = this._userRequestInterceptionEnabled || !!this._credentials?.length; if (enabled === this._protocolRequestInterceptionEnabled) return; this._protocolRequestInterceptionEnabled = enabled; @@ -225,26 +227,20 @@ export class CRNetworkManager { _onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) { let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default'; - const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url); + const credentials = findMatchingHttpCredentials(this._credentials || undefined, event.request.url); if (this._attemptedAuthentications.has(event.requestId)) { response = 'CancelAuth'; - } else if (shouldProvideCredentials) { + } else if (credentials) { response = 'ProvideCredentials'; this._attemptedAuthentications.add(event.requestId); } - const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined }; + const { username, password } = credentials || { username: undefined, password: undefined }; sessionInfo.session._sendMayFail('Fetch.continueWithAuth', { requestId: event.requestId, authChallengeResponse: { response, username, password }, }); } - _shouldProvideCredentials(url: string): boolean { - if (!this._credentials) - return false; - return !this._credentials.origin || new URL(url).origin.toLowerCase() === this._credentials.origin.toLowerCase(); - } - _onRequestPaused(sessionInfo: SessionInfo, event: Protocol.Fetch.requestPausedPayload) { if (!event.networkId) { // Fetch without networkId means that request was not recognized by inspector, and diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 2d32cada41a67..b3f2f520a9c00 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -28,7 +28,7 @@ import { eventsHelper } from '@utils/eventsHelper'; import { monotonicTime } from '@isomorphic/time'; import { createProxyAgent } from '@utils/network'; import { getUserAgent } from './userAgent'; -import { BrowserContext, verifyClientCertificates } from './browserContext'; +import { BrowserContext, findMatchingHttpCredentials, verifyClientCertificates } from './browserContext'; import { Cookie, CookieStore, domainMatches, parseRawCookie } from './cookieStore'; import { MultipartFormData } from './formData'; import { TargetClosedError } from './errors'; @@ -43,7 +43,7 @@ import type { Playwright } from './playwright'; import type { Progress } from './progress'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; -import type { HTTPCredentials } from '../../types/types'; +import type { HttpCredentials } from '@protocol/structs'; import type { RegisteredListener } from '@utils/eventsHelper'; import type * as channels from './channels'; import type * as har from '@trace/har'; @@ -55,7 +55,7 @@ type FetchRequestOptions = { userAgent: string; extraHTTPHeaders?: HeadersArray; failOnStatusCode?: boolean; - httpCredentials?: HTTPCredentials; + httpCredentials?: HttpCredentials[]; proxy?: ProxySettings; ignoreHTTPSErrors?: boolean; maxRedirects?: number; @@ -632,9 +632,7 @@ export abstract class APIRequestContext extends SdkObject { } private _getHttpCredentials(url: URL) { - if (!this._defaultOptions().httpCredentials?.origin || url.origin.toLowerCase() === this._defaultOptions().httpCredentials?.origin?.toLowerCase()) - return this._defaultOptions().httpCredentials; - return undefined; + return findMatchingHttpCredentials(this._defaultOptions().httpCredentials, url.toString()); } } @@ -841,7 +839,7 @@ function isNetworkConnectionError(e: any): boolean { return code === 'ECONNRESET' || code === 'EPIPE' || code === 'ECONNABORTED'; } -function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: HTTPCredentials) { +function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: HttpCredentials) { const { username, password } = credentials; const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64'); setHeader(headers, 'authorization', `Basic ${encoded}`); diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index c02dad2086a79..cf2f84efb89c9 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -28,6 +28,7 @@ import type { SdkObject } from '../instrumentation'; import type { InitScript, Page } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { FFSession } from './ffConnection'; import type { Protocol } from './protocol'; import type * as channels from '../channels'; @@ -317,13 +318,9 @@ export class FFBrowserContext extends BrowserContext { await this._browser.session.send('Browser.setOnlineOverride', { browserContextId: this._browserContextId, override: this._options.offline ? 'offline' : 'online' }); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { this._options.httpCredentials = httpCredentials; - let credentials = null; - if (httpCredentials) { - const { username, password, origin } = httpCredentials; - credentials = [{ username, password, origin }]; - } + const credentials = httpCredentials ? httpCredentials.map(({ username, password, origin }) => ({ username, password, origin })) : null; await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials }); } diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index a834844c09036..52b10d200f5f2 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -58,13 +58,6 @@ export type ScreencastFrame = { viewportHeight: number, }; -export type Credentials = { - username: string; - password: string; - origin?: string; - sendImmediately?: boolean; -}; - export type Geolocation = { longitude: number; latitude: number; diff --git a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts index 77b7f89d98a3d..0e4f37808f245 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts @@ -39,6 +39,7 @@ import type { InitScript, Page } from '../../page'; import type { Protocol } from './protocol'; import type { ProtocolRequest, ProtocolResponse } from '../../transport'; import type * as types from '../../types'; +import type { HttpCredentials } from '@protocol/structs'; import type * as channels from '../../channels'; import type { Progress } from '../../progress'; import type { ConnectOverCDPTransport } from '../../../../types/types.d.ts'; @@ -411,6 +412,6 @@ export class WVBrowserContext extends BrowserContext { override async clearCache(): Promise { throw new Error('Method not implemented.'); } override async doClose(reason: string | undefined): Promise { throw new Error('Method not implemented.'); } override async cancelDownload(uuid: string) { throw new Error('Method not implemented.'); } - protected override async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { throw new Error('Method not implemented.'); } + protected override async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { throw new Error('Method not implemented.'); } protected override async doUpdateOffline(): Promise { throw new Error('Method not implemented.'); } } diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index d315e806c9abd..18b2da2ab203f 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -28,6 +28,7 @@ import type { SdkObject } from '../instrumentation'; import type { InitScript, Page } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { Protocol } from './protocol'; import type { PageProxyMessageReceivedPayload } from './wkConnection'; import type * as channels from '../channels'; @@ -324,7 +325,7 @@ export class WKBrowserContext extends BrowserContext { await (page.delegate as WKPage).updateOffline(); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { this._options.httpCredentials = httpCredentials; for (const page of this.pages()) await (page.delegate as WKPage).updateHttpCredentials(); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 0ef1768254d64..8e6dc38672381 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -752,8 +752,8 @@ export class WKPage implements PageDelegate { } async updateHttpCredentials() { - const credentials = this._browserContext._options.httpCredentials || { username: '', password: '', origin: '' }; - await this._pageProxySession.send('Emulation.setAuthCredentials', { credentials: [{ username: credentials.username, password: credentials.password, origin: credentials.origin }] }); + const credentials = (this._browserContext._options.httpCredentials || []).map(c => ({ username: c.username, password: c.password, origin: c.origin })); + await this._pageProxySession.send('Emulation.setAuthCredentials', { credentials }); } async updateFileChooserInterception() { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index eaa024a641d57..baa5f1d759c4a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10453,13 +10453,28 @@ export interface BrowserContext { /** * @deprecated Browsers may cache credentials after successful authentication. Create a new browser context instead. - * @param httpCredentials + * @param httpCredentials Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ setHTTPCredentials(httpCredentials: null|{ username: string; password: string; - }): Promise; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }|ReadonlyArray<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }>): Promise; /** * @param offline Whether to emulate network being offline for the browser context. @@ -11229,6 +11244,9 @@ export interface Browser { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -11248,7 +11266,25 @@ export interface Browser { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -17270,6 +17306,9 @@ export interface BrowserType { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -17289,7 +17328,25 @@ export interface BrowserType { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * If `true`, Playwright does not pass its own configurations args and only uses the ones from @@ -18885,6 +18942,9 @@ export interface APIRequest { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -18904,7 +18964,25 @@ export interface APIRequest { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -23588,6 +23666,9 @@ export interface Electron { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -23607,7 +23688,25 @@ export interface Electron { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -24225,6 +24324,9 @@ export interface AndroidDevice { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -24244,7 +24346,25 @@ export interface AndroidDevice { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -25457,8 +25577,11 @@ export interface BrowserContextOptions { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ - httpCredentials?: HTTPCredentials; + httpCredentials?: HTTPCredentials|Array; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 7a1ec114f0b33..bcabe8b32f84f 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -7446,6 +7446,9 @@ export interface PlaywrightTestOptions { * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. * + * Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. + * * **Usage** * * ```js @@ -7463,7 +7466,7 @@ export interface PlaywrightTestOptions { * ``` * */ - httpCredentials: HTTPCredentials | undefined; + httpCredentials: HTTPCredentials | HTTPCredentials[] | undefined; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. * diff --git a/packages/protocol/spec/browserContext.yml b/packages/protocol/spec/browserContext.yml index c87e60bd75af2..5c1505513ccf6 100644 --- a/packages/protocol/spec/browserContext.yml +++ b/packages/protocol/spec/browserContext.yml @@ -136,11 +136,8 @@ BrowserContext: group: configuration parameters: httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? + type: array? + items: HttpCredentials setNetworkInterceptionPatterns: title: Route requests diff --git a/packages/protocol/spec/electron.yml b/packages/protocol/spec/electron.yml index 877b4077469af..41a659831f60a 100644 --- a/packages/protocol/spec/electron.yml +++ b/packages/protocol/spec/electron.yml @@ -52,11 +52,8 @@ Electron: latitude: float accuracy: float? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? + type: array? + items: HttpCredentials ignoreHTTPSErrors: boolean? locale: string? offline: boolean? diff --git a/packages/protocol/spec/mixins.yml b/packages/protocol/spec/mixins.yml index 36c197654b6ca..80dcdfe8f47fb 100644 --- a/packages/protocol/spec/mixins.yml +++ b/packages/protocol/spec/mixins.yml @@ -94,6 +94,18 @@ LaunchOptions: firefoxUserPrefs: json? cdpPort: int? +HttpCredentials: + type: object + properties: + username: string + password: string + origin: string? + send: + type: enum? + literals: + - always + - unauthorized + ContextOptions: type: mixin properties: @@ -138,16 +150,8 @@ ContextOptions: items: NameValue offline: boolean? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized + type: array? + items: HttpCredentials deviceScaleFactor: float? isMobile: boolean? hasTouch: boolean? diff --git a/packages/protocol/spec/playwright.yml b/packages/protocol/spec/playwright.yml index de3634d75b1fb..7eac6215db960 100644 --- a/packages/protocol/spec/playwright.yml +++ b/packages/protocol/spec/playwright.yml @@ -64,16 +64,8 @@ Playwright: pfx: binary? maxRedirects: int? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized + type: array? + items: HttpCredentials proxy: type: object? properties: diff --git a/packages/protocol/src/structs.d.ts b/packages/protocol/src/structs.d.ts index 30ee9c10c1149..9ee0c14461818 100644 --- a/packages/protocol/src/structs.d.ts +++ b/packages/protocol/src/structs.d.ts @@ -110,6 +110,13 @@ export type WaitInfo = { error?: string, }; +export type HttpCredentials = { + username: string, + password: string, + origin?: string, + send?: 'always' | 'unauthorized', +}; + export type SetNetworkCookie = { name: string, value: string, diff --git a/packages/protocol/src/validator.ts b/packages/protocol/src/validator.ts index d4c876cb078bf..4d09a0d987641 100644 --- a/packages/protocol/src/validator.ts +++ b/packages/protocol/src/validator.ts @@ -172,12 +172,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -458,12 +453,7 @@ scheme.BrowserNewContextParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -536,12 +526,7 @@ scheme.BrowserNewContextForReuseParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -636,12 +621,7 @@ scheme.BrowserContextInitializer = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -808,11 +788,7 @@ scheme.BrowserContextSetGeolocationParams = tObject({ }); scheme.BrowserContextSetGeolocationResult = tOptional(tObject({})); scheme.BrowserContextSetHTTPCredentialsParams = tObject({ - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), }); scheme.BrowserContextSetHTTPCredentialsResult = tOptional(tObject({})); scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({ @@ -1038,12 +1014,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -1138,11 +1109,7 @@ scheme.ElectronLaunchParams = tObject({ latitude: tFloat, accuracy: tOptional(tFloat), })), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), ignoreHTTPSErrors: tOptional(tBoolean), locale: tOptional(tString), offline: tOptional(tBoolean), @@ -2013,6 +1980,12 @@ scheme.LocalUtilsGlobToRegexParams = tObject({ scheme.LocalUtilsGlobToRegexResult = tObject({ regex: tString, }); +scheme.HttpCredentials = tObject({ + username: tString, + password: tString, + origin: tOptional(tString), + send: tOptional(tEnum(['always', 'unauthorized'])), +}); scheme.SetNetworkCookie = tObject({ name: tString, value: tString, @@ -2687,12 +2660,7 @@ scheme.PlaywrightNewRequestParams = tObject({ pfx: tOptional(tBinary), }))), maxRedirects: tOptional(tInt), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), proxy: tOptional(tObject({ server: tString, bypass: tOptional(tString), diff --git a/tests/library/browsercontext-credentials.spec.ts b/tests/library/browsercontext-credentials.spec.ts index 20e285c568252..4713245ff90a3 100644 --- a/tests/library/browsercontext-credentials.spec.ts +++ b/tests/library/browsercontext-credentials.spec.ts @@ -51,6 +51,22 @@ it('should work with setHTTPCredentials', async ({ browser, server, failsOn401 } await context.close(); }); +it('should work with setHTTPCredentials and multiple credentials', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const context = await browser.newContext(); + const page = await context.newPage(); + await context.setHTTPCredentials([ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ]); + const response1 = await page.goto(server.EMPTY_PAGE); + expect(response1!.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2!.status()).toBe(401); + await context.close(); +}); + it('should work with correct credentials @smoke', async ({ browser, server }) => { server.setAuth('/empty.html', 'user', 'pass'); const context = await browser.newContext({ @@ -86,6 +102,65 @@ it('should return resource body', async ({ browser, server }) => { await context.close(); }); +it('should work with a single credential in an array', async ({ browser, server }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: [{ username: 'user', password: 'pass' }] + }); + const page = await context.newPage(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(200); + await context.close(); +}); + +it('should work with multiple credentials for different origins', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const context = await browser.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + const page = await context.newPage(); + const response1 = await page.goto(server.EMPTY_PAGE); + expect(response1!.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2!.status()).toBe(401); + await context.close(); +}); + +it('should fall back to credentials without origin', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: [ + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + { username: 'user', password: 'pass' }, + ] + }); + const page = await context.newPage(); + const response1 = await page.goto(server.EMPTY_PAGE); + expect(response1!.status()).toBe(200); + // First matching entry has wrong credentials for this origin. + const response2 = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2!.status()).toBe(401); + await context.close(); +}); + +it('should use the first matching credential', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: [ + { username: 'wrong', password: 'wrong' }, + { username: 'user', password: 'pass', origin: server.PREFIX }, + ] + }); + const page = await context.newPage(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(401); + await context.close(); +}); + it('should work with correct credentials and matching origin', async ({ browser, server }) => { server.setAuth('/empty.html', 'user', 'pass'); const context = await browser.newContext({ diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 54f219d2cd0c6..64d54e58dcd30 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -563,6 +563,21 @@ it('should return error with wrong credentials', async ({ context, server }) => expect(response2.status()).toBe(401); }); +it('should support multiple httpCredentials', async ({ contextFactory, server, browserName }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const context = await contextFactory({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + const response1 = await context.request.get(server.EMPTY_PAGE); + expect(response1.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await context.request.get(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2.status()).toBe(401); +}); + it('should support HTTPCredentials.send for newContext', async ({ contextFactory, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' }); const context = await contextFactory({ diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts index d980b253f0f2a..8a92169fab4f0 100644 --- a/tests/library/global-fetch.spec.ts +++ b/tests/library/global-fetch.spec.ts @@ -220,6 +220,49 @@ it('should support HTTPCredentials.send', async ({ playwright, server }) => { await request.dispose(); }); +it('should support multiple httpCredentials', async ({ playwright, server }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const request = await playwright.request.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + const response1 = await request.get(server.EMPTY_PAGE); + expect(response1.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await request.get(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2.status()).toBe(401); + await request.dispose(); +}); + +it('should support HTTPCredentials.send with multiple httpCredentials', async ({ playwright, server }) => { + const request = await playwright.request.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX, send: 'always' }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX, send: 'unauthorized' }, + ] + }); + { + const [serverRequest, response] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user1:pass1').toString('base64')); + expect(response.status()).toBe(200); + } + { + const [serverRequest, response] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.CROSS_PROCESS_PREFIX + '/empty.html') + ]); + // This origin has send: 'unauthorized', so credentials are not sent proactively. + expect(serverRequest.headers.authorization).toBe(undefined); + expect(response.status()).toBe(200); + } + await request.dispose(); +}); + it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => { const request = await playwright.request.newContext({ ignoreHTTPSErrors: true }); const response = await request.get(httpsServer.EMPTY_PAGE); diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js index 1fbc1bfe9003d..3858d8f9bc44d 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -892,6 +892,9 @@ function csharpOptionOverloadSuffix(option, type) { case 'int': return 'Int'; case 'long': return 'Int64'; case 'Date': return 'Date'; + // Object keeps the original option name, e.g. for `Object|Array` unions. + case 'Object': return ''; + case 'Array': return 'List'; } throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`); } diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index ee7f2e13246f6..48849be555e6e 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -280,7 +280,7 @@ export interface PlaywrightTestOptions { extraHTTPHeaders: ExtraHTTPHeaders | undefined; geolocation: Geolocation | undefined; hasTouch: boolean; - httpCredentials: HTTPCredentials | undefined; + httpCredentials: HTTPCredentials | HTTPCredentials[] | undefined; ignoreHTTPSErrors: boolean; isMobile: boolean; javaScriptEnabled: boolean;