This commit is contained in:
Max Schmitt 2024-07-09 17:27:17 +02:00
parent 918ead80ec
commit 4a79602815
7 changed files with 44 additions and 28 deletions

View file

@ -521,7 +521,7 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.
- `cert` ?<[string]> Path to the file with the certificate in PEM format. - `cert` ?<[string]> Path to the file with the certificate in PEM format.
- `key` ?<[string]> Path to the file with the private key in PEM format. - `key` ?<[string]> Path to the file with the private key in PEM format.
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX). - `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
- `pfx` ?<[string]> PFX or PKCS12 encoded private key and certificate chain. - `pfx` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
An array of client certificates to be used. Each certificate object must have `cert` and `key` or `pfx` to load the client certificate. Optionally, `passphrase` property should be provided if the private key is encrypted. If the certificate is issued by a custom certificate authority, the `ignoreHTTPSErrors` needs to be set. If the certificate is valid only for specific URLs, the `url` property should be provided with a glob pattern to match the URLs that the certificate is valid for. An array of client certificates to be used. Each certificate object must have `cert` and `key` or `pfx` to load the client certificate. Optionally, `passphrase` property should be provided if the private key is encrypted. If the certificate is issued by a custom certificate authority, the `ignoreHTTPSErrors` needs to be set. If the certificate is valid only for specific URLs, the `url` property should be provided with a glob pattern to match the URLs that the certificate is valid for.

View file

@ -529,7 +529,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion, reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion,
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors, forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
clientCertificates: await toClientCertificatesProtocol(options.proxy, options.clientCertificates), clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
}; };
if (!contextParams.recordVideo && options.videosPath) { if (!contextParams.recordVideo && options.videosPath) {
contextParams.recordVideo = { contextParams.recordVideo = {
@ -550,25 +550,13 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
return 'deny'; return 'deny';
} }
export async function toClientCertificatesProtocol(proxy: BrowserContextOptions['proxy'], clientCertificates?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> { export async function toClientCertificatesProtocol(clientCertificates?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!clientCertificates) if (!clientCertificates)
return undefined; return undefined;
if (proxy)
throw new Error('Cannot specify both proxy and clientCertificates');
return await Promise.all(clientCertificates.map(async clientCertificate => { return await Promise.all(clientCertificates.map(async clientCertificate => {
if (clientCertificate.certs.length === 0)
throw new Error('No certs specified for url: ' + clientCertificate.url);
return { return {
url: clientCertificate.url, url: clientCertificate.url,
certs: await Promise.all(clientCertificate.certs.map(async cert => { certs: await Promise.all(clientCertificate.certs.map(async cert => {
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
throw new Error('None of cert, key, passphrase or pfx is specified');
if (cert.cert && !cert.key)
throw new Error('cert is specified without key');
if (!cert.cert && cert.key)
throw new Error('key is specified without cert');
if (cert.pfx && (cert.cert || cert.key || cert.passphrase))
throw new Error('pfx is specified together with cert, key or passphrase');
return { return {
cert: cert.cert ? await fs.promises.readFile(cert.cert) : undefined, cert: cert.cert ? await fs.promises.readFile(cert.cert) : undefined,
key: cert.key ? await fs.promises.readFile(cert.key) : undefined, key: cert.key ? await fs.promises.readFile(cert.key) : undefined,

View file

@ -76,7 +76,7 @@ export class APIRequest implements api.APIRequest {
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
storageState, storageState,
tracesDir, tracesDir,
clientCertificates: await toClientCertificatesProtocol(options.proxy, options.clientCertificates), clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
})).request); })).request);
this._contexts.add(context); this._contexts.add(context);
context._request = this; context._request = this;

View file

@ -689,11 +689,14 @@ export function validateBrowserContextOptions(options: channels.BrowserNewContex
if (options.proxy) { if (options.proxy) {
if (!browserOptions.proxy && browserOptions.isChromium && os.platform() === 'win32') if (!browserOptions.proxy && browserOptions.isChromium && os.platform() === 'win32')
throw new Error(`Browser needs to be launched with the global proxy. If all contexts override the proxy, global proxy will be never used and can be any string, for example "launch({ proxy: { server: 'http://per-context' } })"`); throw new Error(`Browser needs to be launched with the global proxy. If all contexts override the proxy, global proxy will be never used and can be any string, for example "launch({ proxy: { server: 'http://per-context' } })"`);
if (options.clientCertificates)
throw new Error('Cannot specify both proxy and clientCertificates');
options.proxy = normalizeProxySettings(options.proxy); options.proxy = normalizeProxySettings(options.proxy);
} }
if (options.clientCertificates?.length) if (options.clientCertificates?.length)
options.ignoreHTTPSErrors = true; options.ignoreHTTPSErrors = true;
verifyGeolocation(options.geolocation); verifyGeolocation(options.geolocation);
verifyClientCertificates(options.clientCertificates);
} }
export function verifyGeolocation(geolocation?: types.Geolocation) { export function verifyGeolocation(geolocation?: types.Geolocation) {
@ -709,6 +712,27 @@ export function verifyGeolocation(geolocation?: types.Geolocation) {
throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`); throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`);
} }
export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) {
if (!clientCertificates)
return;
for (const { url, certs } of clientCertificates) {
if (!url)
throw new Error(`clientCertificates.url is required`);
if (!certs.length)
throw new Error('No certs specified for url: ' + url);
for (const cert of certs) {
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
throw new Error('None of cert, key, passphrase or pfx is specified');
if (cert.cert && !cert.key)
throw new Error('cert is specified without key');
if (!cert.cert && cert.key)
throw new Error('key is specified without cert');
if (cert.pfx && (cert.cert || cert.key || cert.passphrase))
throw new Error('pfx is specified together with cert, key or passphrase');
}
}
}
export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxySettings { export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxySettings {
let { server, bypass } = proxy; let { server, bypass } = proxy;
let url; let url;

View file

@ -27,7 +27,7 @@ import { TimeoutSettings } from '../common/timeoutSettings';
import { getUserAgent } from '../utils/userAgent'; import { getUserAgent } from '../utils/userAgent';
import { assert, createGuid, monotonicTime } from '../utils'; import { assert, createGuid, monotonicTime } from '../utils';
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
import { BrowserContext } from './browserContext'; import { BrowserContext, verifyClientCertificates } from './browserContext';
import { CookieStore, domainMatches } from './cookieStore'; import { CookieStore, domainMatches } from './cookieStore';
import { MultipartFormData } from './formData'; import { MultipartFormData } from './formData';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs'; import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
@ -562,11 +562,14 @@ export class GlobalAPIRequestContext extends APIRequestContext {
if (!/^\w+:\/\//.test(url)) if (!/^\w+:\/\//.test(url))
url = 'http://' + url; url = 'http://' + url;
proxy.server = url; proxy.server = url;
if (options.clientCertificates)
throw new Error('Cannot specify both proxy and clientCertificates');
} }
if (options.storageState) { if (options.storageState) {
this._origins = options.storageState.origins; this._origins = options.storageState.origins;
this._cookieStore.addCookies(options.storageState.cookies || []); this._cookieStore.addCookies(options.storageState.cookies || []);
} }
verifyClientCertificates(options.clientCertificates);
this._options = { this._options = {
baseURL: options.baseURL, baseURL: options.baseURL,
userAgent: options.userAgent || getUserAgent(), userAgent: options.userAgent || getUserAgent(),

View file

@ -15625,7 +15625,7 @@ export interface APIRequest {
passphrase?: string; passphrase?: string;
/** /**
* PFX or PKCS12 encoded private key and certificate chain. * Path to the PFX or PKCS12 encoded private key and certificate chain.
*/ */
pfx?: string; pfx?: string;
}>; }>;
@ -16817,7 +16817,7 @@ export interface Browser extends EventEmitter {
passphrase?: string; passphrase?: string;
/** /**
* PFX or PKCS12 encoded private key and certificate chain. * Path to the PFX or PKCS12 encoded private key and certificate chain.
*/ */
pfx?: string; pfx?: string;
}>; }>;
@ -20290,7 +20290,7 @@ export interface BrowserContextOptions {
passphrase?: string; passphrase?: string;
/** /**
* PFX or PKCS12 encoded private key and certificate chain. * Path to the PFX or PKCS12 encoded private key and certificate chain.
*/ */
pfx?: string; pfx?: string;
}>; }>;

View file

@ -59,16 +59,17 @@ const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: str
test.skip(({ mode }) => mode !== 'default'); test.skip(({ mode }) => mode !== 'default');
const kClientCertificatesDir = path.join(__dirname, '../config/testserver/client-certificates'); const kClientCertificatesDir = path.join(__dirname, '../config/testserver/client-certificates');
const kDummyFileName = __filename;
const kValidationSubTests: [BrowserContextOptions, string ][] = [ const kValidationSubTests: [BrowserContextOptions, string ][] = [
[{ clientCertificates: [{ url: 'test', certs: [] }] }, 'No certs specified for url: test'], [{ clientCertificates: [{ url: 'test', certs: [] }] }, 'No certs specified for url: test'],
[{ clientCertificates: [{ url: 'test', certs: [{}] }] }, 'None of cert, key, passphrase or pfx is specified'], [{ clientCertificates: [{ url: 'test', certs: [{}] }] }, 'None of cert, key, passphrase or pfx is specified'],
[{ clientCertificates: [{ [{ clientCertificates: [{
url: 'test', url: 'test',
certs: [{ certs: [{
cert: 'foo', cert: kDummyFileName,
key: 'foo', key: kDummyFileName,
passphrase: 'foo', passphrase: kDummyFileName,
pfx: 'foo', pfx: kDummyFileName,
}] }]
}] }, 'pfx is specified together with cert, key or passphrase'], }] }, 'pfx is specified together with cert, key or passphrase'],
[{ [{
@ -76,10 +77,10 @@ const kValidationSubTests: [BrowserContextOptions, string ][] = [
clientCertificates: [{ clientCertificates: [{
url: 'test', url: 'test',
certs: [{ certs: [{
cert: 'foo', cert: kDummyFileName,
key: 'foo', key: kDummyFileName,
passphrase: 'foo', passphrase: kDummyFileName,
pfx: 'foo', pfx: kDummyFileName,
}] }]
}] }, 'Cannot specify both proxy and clientCertificates'], }] }, 'Cannot specify both proxy and clientCertificates'],
]; ];