feat: support client certificates

This commit is contained in:
Max Schmitt 2024-07-04 11:05:47 +02:00
parent 97e2aa07a5
commit e995f4a6f9
30 changed files with 1319 additions and 7 deletions

View file

@ -12,6 +12,9 @@ see [APIRequestContext].
Creates new instances of [APIRequestContext].
### option: APIRequest.newContext.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
### option: APIRequest.newContext.useragent = %%-context-option-useragent-%%
* since: v1.16

View file

@ -256,6 +256,9 @@ await browser.CloseAsync();
### option: Browser.newContext.proxy = %%-context-option-proxy-%%
* since: v1.8
### option: Browser.newContext.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
### option: Browser.newContext.storageState = %%-js-python-context-option-storage-state-%%
* since: v1.8
@ -281,6 +284,9 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
### option: Browser.newPage.proxy = %%-context-option-proxy-%%
* since: v1.8
### option: Browser.newPage.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
### option: Browser.newPage.storageState = %%-js-python-context-option-storage-state-%%
* since: v1.8

View file

@ -514,6 +514,17 @@ Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_
Does not enforce fixed viewport, allows resizing window in the headed mode.
## context-option-clientCertificates
- `clientCertificates` <[Array]<[Object]>>
- `url` <[string]> Glob pattern to match the URLs that the certificate is valid for.
- `certs` <[Array]<[Object]>> List of client certificates to be used.
- `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.
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
- `pfx` ?<[string]> 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 `ca` property should be provided with the path to the file with the certificate authority's certificate. 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.
## context-option-useragent
- `userAgent` <[string]>

View file

@ -138,6 +138,35 @@ export default defineConfig({
]
});
```
## property: TestOptions.clientCertificates = %%-context-option-clientCertificates-%%
* since: 1.46
**Usage**
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'Microsoft Edge',
use: {
...devices['Desktop Edge'],
clientCertificates: [{
url: 'https://example.com',
certs: [{
cert: 'client/alice_cert.pem',
key: 'client/alice_key.pem',
passphase: 'mysecretpassword',
}],
}],
},
},
]
});
```
## property: TestOptions.colorScheme = %%-context-option-colorscheme-%%
* since: v1.10

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUTcrzEueVL/OuLHr4LBIPWeS4UL0wDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDcwNDA4NDAzNFoXDTM0MDcw
MjA4NDAzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEApof+SZVN4UGma4xJDVHhMSpmEJoCdMPr+HFadJJK/brF
BNOhA1C5wNk8oD/XYo7enAHQH/EsBnq4MMxv79rXTGnIdXMF+43GdMDh5kh81FQy
Esw8Vt4eif9eZkjUxI2GHhR2ovJewmQa7E+SeUB2RzJTqz8QPLhd74JFfgaci+S2
8L37ScVjcw55T1PcNflzB4vwsQHBT3yND0MLDhm+8MLzmTl4Mw5PgIOaBl5Jh8Tr
wQF4eeeB3FPJoMQhTP8aGBjW1mo+NmSSRAPIAZyhmCAnDeC33yRjAaiHjaL5Pr9f
wt5zoF5+U1xWhGXWzGOE6p/VTj62F9a2fOXNHclYJQIDAQABo1MwUTAdBgNVHQ4E
FgQU9BoVzGtb5x70KqGO/89N1hyqi5kwHwYDVR0jBBgwFoAU9BoVzGtb5x70KqGO
/89N1hyqi5kwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYcbI
wvcfx2p8z0RNN3EA+epKX1SagZyJX4ORIO8kln1sDU+ceHde3n3xnp1dg6HG2qh1
a7CZub/fNUaP9R8+6iiV0wPT7Ybkb2NIJcH1yq+/bfSS5OC5DO0yv9SUADdBoDwa
zOuBAqdcYW1BHYcbAzsQnniRcejHu06ioaS6SwwJ8150rQnLT4Lh9LAl40W6v4nZ
NdTGQETTrbjcgH1ER4IhWTKtVyPOxGF9A/OOawMEdfS8BhUO7YRS4QNFFaQMrJAb
MDhDtjSyDogLr8P43xjjWvQWG9a7zTF0kKEsdJ0cEG5HATpg8bPHmrouxbs2HGeH
kJXzMykrsYyXsInN3w==
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmh/5JlU3hQaZr
jEkNUeExKmYQmgJ0w+v4cVp0kkr9usUE06EDULnA2TygP9dijt6cAdAf8SwGergw
zG/v2tdMach1cwX7jcZ0wOHmSHzUVDISzDxW3h6J/15mSNTEjYYeFHai8l7CZBrs
T5J5QHZHMlOrPxA8uF3vgkV+BpyL5LbwvftJxWNzDnlPU9w1+XMHi/CxAcFPfI0P
QwsOGb7wwvOZOXgzDk+Ag5oGXkmHxOvBAXh554HcU8mgxCFM/xoYGNbWaj42ZJJE
A8gBnKGYICcN4LffJGMBqIeNovk+v1/C3nOgXn5TXFaEZdbMY4Tqn9VOPrYX1rZ8
5c0dyVglAgMBAAECggEAB6zX4vNPKhUZAvbtvP/rlZUDLDu05kXLX+F1jk7ZxvTv
NKg+UQVM8l7wxN/8YM3944nP2lEGuuu4BoO9mvvmlV6Avy0EdxITNflX0AHCQxT4
U9Z253gIR0ruQl+T8tUk+8jsqNjr1iC//ukx8oWujdx7b7aR3IKQzcOeyU6rs2TN
lyrVVsEaFVi9+wCw0xyiCmPlobrn+egdigw7Zhp2BRinC6W9eMxuPS2hlhQUhBm/
eiD96YWp0RAv/L5qO93reoXIAzrrLdcUgPEnnq1zN7y2xihU2+B2sTph1m/A26+J
yPcXd7vQrXlRXQU6PaCa+0oJULlpiAzy3HPbnr4BkQKBgQDdmekTX8dQqiEZPX1C
017QRFbx0/x/TDFDSeJbDeauMzzCaGqCO2WVmYmTvFtby2G4/6BYowVtJVHm4uJl
XsYk8dWIQGLPIj1Cw7ZieJvb2EVRxgnY2oMaOTOazHzPHFzZV718zwEeZrryT82J
881E8wgM8V3DjkS4ye3TbwvimQKBgQDAYa/IdnpAg5z1TREi9Tt8fnoGpmSscAak
USgeXVsvoNzXXkE94MiiCOOrX1r68TWYDAzq6MKGDewkWOfLwXWR6D5C2LyE1q9P
1pxstgs/nC3ZUTz0yEH47ahSmhywhGlvXXOQEXUSLiVTOdeMCubMqwQW80F1868n
aBHcj5/lbQKBgQDIojjsWaNT3TTqbUmj30vQtI8jlBLgDlPr4FEYr5VT0wAH5BHK
p4xpzgFJyRfOHG312TuMBM087LUinfjsXsp3WJ1EJ0dO0mk0sY3HyfsTKNRaHTt9
Ixnf/DpExS+bNMq73Tyqa6FPrSNFkAtAA4SuEHwRe9aw33ZI+EpjS/8uwQKBgQCi
9NwqSLlLVnColEw0uVdXH+cLJPzX19i4bQo3lkp8MJ2ATJWk7XflUPRQoGf3ckQ8
c9CpVtoXJUnmi+xkeo21Nu0uQFqHhzZewWIk75rdmdR4ZUjl649+ZQkUVviASNjq
fVU7Lp5k9POm6LL9K+rOaPoA2rKTUAQItC2VD4+YjQKBgB6kgvgN6Mz/u0RE3kkV
2GOoP5sso71Hxwh7o6JEzUMhR+e/T/LLcBwEjLYcf1FYRySHsXLn2Ar/Uw1J7pAZ
ud54/at+7mTDliaT8Ar7S9vcso7ZfmuDX9qB9+c77idPskVBPo2tjJbwvFcB6sww
5Elcfmj6tEP4YLJ6Kv3qTPhT
-----END PRIVATE KEY-----

View file

@ -28,7 +28,7 @@ import { Worker } from './worker';
import { Events } from './events';
import { TimeoutSettings } from '../common/timeoutSettings';
import { Waiter } from './waiter';
import type { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
import type { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions, ClientCertificate } from './types';
import { headersObjectToArray, isRegExp, isString, urlMatchesEqual } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import type * as api from '../../types/types';
@ -529,6 +529,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion,
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
};
if (!contextParams.recordVideo && options.videosPath) {
contextParams.recordVideo = {
@ -548,3 +549,31 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
return 'accept';
return 'deny';
}
export async function toClientCertificatesProtocol(clientCertificates?: ClientCertificate[]): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!clientCertificates)
return undefined;
return await Promise.all(clientCertificates.map(async clientCertificate => {
if (clientCertificate.certs.length === 0)
throw new Error('No certs specified for url: ' + clientCertificate.url);
return {
url: clientCertificate.url,
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 {
cert: cert.cert ? await fs.promises.readFile(cert.cert) : undefined,
key: cert.key ? await fs.promises.readFile(cert.key) : undefined,
passphrase: cert.passphrase,
pfx: cert.pfx ? await fs.promises.readFile(cert.pfx) : undefined,
};
}))
};
}));
}

View file

@ -25,10 +25,11 @@ import { assert, headersObjectToArray, isString } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import { ChannelOwner } from './channelOwner';
import { RawHeaders } from './network';
import type { FilePayload, Headers, StorageState } from './types';
import type { ClientCertificate, FilePayload, Headers, StorageState } from './types';
import type { Playwright } from './playwright';
import { Tracing } from './tracing';
import { TargetClosedError, isTargetClosedError } from './errors';
import { toClientCertificatesProtocol } from './browserContext';
export type FetchOptions = {
params?: { [key: string]: string; },
@ -44,9 +45,10 @@ export type FetchOptions = {
maxRetries?: number,
};
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'storageState' | 'tracesDir'> & {
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
extraHTTPHeaders?: Headers,
storageState?: string | StorageState,
clientCertificates?: ClientCertificate[];
};
type RequestWithBodyOptions = Omit<FetchOptions, 'method'>;
@ -74,6 +76,7 @@ export class APIRequest implements api.APIRequest {
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
storageState,
tracesDir,
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
})).request);
this._contexts.add(context);
context._request = this;
@ -175,7 +178,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
const params = objectToArray(options.params);
const method = options.method || options.request?.method();
// Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options.headers || options.request?.headers() ;
const headersObj = options.headers || options.request?.headers();
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
let jsonData: any;
let formData: channels.NameValue[] | undefined;
@ -395,7 +398,7 @@ function isJsonContentType(headers?: HeadersArray): boolean {
return false;
}
function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
if (!map)
return undefined;
const result = [];

View file

@ -47,7 +47,17 @@ export type SetStorageState = {
export type LifecycleEvent = channels.LifecycleEvent;
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']);
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads'> & {
export type ClientCertificate = {
url: string;
certs: {
cert?: string;
key?: string;
passphrase?: string;
pfx?: string;
}[];
};
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads'> & {
viewport?: Size | null;
extraHTTPHeaders?: Headers;
logger?: Logger;
@ -70,6 +80,7 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
reducedMotion?: 'reduce' | 'no-preference' | null;
forcedColors?: 'active' | 'none' | null;
acceptDownloads?: boolean;
clientCertificates?: ClientCertificate[];
};
type LaunchOverrides = {

View file

@ -331,6 +331,15 @@ scheme.PlaywrightNewRequestParams = tObject({
userAgent: tOptional(tString),
ignoreHTTPSErrors: tOptional(tBoolean),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
}))),
httpCredentials: tOptional(tObject({
username: tString,
password: tString,
@ -532,6 +541,15 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
height: tNumber,
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
userAgent: tOptional(tString),
@ -614,6 +632,15 @@ scheme.BrowserNewContextParams = tObject({
height: tNumber,
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
userAgent: tOptional(tString),
@ -676,6 +703,15 @@ scheme.BrowserNewContextForReuseParams = tObject({
height: tNumber,
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
userAgent: tOptional(tString),
@ -2513,6 +2549,15 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
height: tNumber,
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
userAgent: tOptional(tString),

View file

@ -25,6 +25,7 @@ import type { RecentLogsCollector } from '../utils/debugLogger';
import type { CallMetadata } from './instrumentation';
import { SdkObject } from './instrumentation';
import { Artifact } from './artifact';
import { ClientCertificatesProxy, shouldUseMitmSocksProxy } from './socksClientCertificatesInterceptor';
export interface BrowserProcess {
onclose?: ((exitCode: number | null, signal: string | null) => void);
@ -84,7 +85,13 @@ export abstract class Browser extends SdkObject {
async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
let clientCertificateProxy: ClientCertificatesProxy | undefined;
if (shouldUseMitmSocksProxy(options)) {
clientCertificateProxy = new ClientCertificatesProxy(options);
options.proxy = { server: await clientCertificateProxy.listen() };
}
const context = await this.doCreateNewContext(options);
context._socksServer = clientCertificateProxy;
if (options.storageState)
await context.setStorageState(metadata, options.storageState);
return context;

View file

@ -43,6 +43,7 @@ import * as consoleApiSource from '../generated/consoleApiSource';
import { BrowserContextAPIRequestContext } from './fetch';
import type { Artifact } from './artifact';
import { Clock } from './clock';
import { shouldUseMitmSocksProxy, type ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
export abstract class BrowserContext extends SdkObject {
static Events = {
@ -90,6 +91,7 @@ export abstract class BrowserContext extends SdkObject {
private _debugger!: Debugger;
_closeReason: string | undefined;
readonly clock: Clock;
_socksServer: ClientCertificatesProxy | undefined;
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
super(browser, 'browser-context');
@ -447,6 +449,8 @@ export abstract class BrowserContext extends SdkObject {
await harRecorder.flush();
await this.tracing.flush();
await this._socksServer?.close();
// Cleanup.
const promises: Promise<void>[] = [];
for (const { context, artifact } of this._browser._idToVideo.values()) {
@ -687,6 +691,8 @@ export function validateBrowserContextOptions(options: channels.BrowserNewContex
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' } })"`);
options.proxy = normalizeProxySettings(options.proxy);
}
if (shouldUseMitmSocksProxy(options))
options.ignoreHTTPSErrors = true;
verifyGeolocation(options.geolocation);
}

View file

@ -40,6 +40,7 @@ import { Tracing } from './trace/recorder/tracing';
import type * as types from './types';
import type { HeadersArray, ProxySettings } from './types';
import { kMaxCookieExpiresDateInSeconds } from './network';
import { clientCertificatesToTLSOptions } from './socksClientCertificatesInterceptor';
type FetchRequestOptions = {
userAgent: string;
@ -49,6 +50,7 @@ type FetchRequestOptions = {
timeoutSettings: TimeoutSettings;
ignoreHTTPSErrors?: boolean;
baseURL?: string;
clientCertificates?: channels.BrowserNewContextOptions['clientCertificates'];
};
type HeadersObject = Readonly<{ [name: string]: string }>;
@ -190,9 +192,10 @@ export abstract class APIRequestContext extends SdkObject {
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
timeout,
deadline,
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.toString()),
__testHookLookup: (params as any).__testHookLookup,
};
// rejectUnauthorized = undefined is treated as true in node 12.
// rejectUnauthorized = undefined is treated as true in Node.js 12.
if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors)
options.rejectUnauthorized = false;
@ -351,6 +354,7 @@ export abstract class APIRequestContext extends SdkObject {
maxRedirects: options.maxRedirects - 1,
timeout: options.timeout,
deadline: options.deadline,
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.toString()),
__testHookLookup: options.__testHookLookup,
};
// rejectUnauthorized = undefined is treated as true in node 12.
@ -522,6 +526,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
timeoutSettings: this._context._timeoutSettings,
ignoreHTTPSErrors: this._context._options.ignoreHTTPSErrors,
baseURL: this._context._options.baseURL,
clientCertificates: this._context._options.clientCertificates,
};
}
@ -568,6 +573,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
extraHTTPHeaders: options.extraHTTPHeaders,
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
httpCredentials: options.httpCredentials,
clientCertificates: options.clientCertificates,
proxy,
timeoutSettings,
};

View file

@ -0,0 +1,203 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type net from 'net';
import path from 'path';
import type https from 'https';
import fs from 'fs';
import tls from 'tls';
import stream from 'stream';
import { createSocket } from '../utils/happy-eyeballs';
import { assert, isUnderTest, urlMatches } from '../utils';
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
import { SocksProxy } from '../common/socksProxy';
import type * as channels from '@protocol/channels';
class SocksConnectionDuplex extends stream.Duplex {
constructor(private readonly writeCallback: (data: Buffer) => void) {
super();
}
override _read(): void {}
override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void): void {
this.writeCallback(chunk);
callback();
}
}
class SocksProxyConnection {
firstPackageReceived: boolean = false;
isTLS: boolean = false;
target: net.Socket = null!;
// In case of http, we just pipe data to the target socket and they are |undefined|.
internal: stream.Duplex | undefined;
internalTLS: tls.TLSSocket | undefined;
constructor(private readonly socksProxy: ClientCertificatesProxy, private readonly uid: string, private readonly host: string, private readonly port: number) {}
async connect() {
// WebKit on Darwin doesn't proxy localhost requests through the given proxy.
// Workaround: Rewrite to localhost during tests.
this.target = await createSocket(isUnderTest() ? 'localhost' : this.host, this.port);
this.target.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
this.target.on('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
this.socksProxy._socksProxy.socketConnected({
uid: this.uid,
host: this.target.localAddress!,
port: this.target.localPort!,
});
}
public onClose() {
this.internal?.destroy();
this.target.destroy();
}
public onData(data: Buffer) {
// HTTP / TLS are client-hello based protocols. This allows us to detect
// the protocol on the first package and attach appropriate listeners.
if (!this.firstPackageReceived) {
this.firstPackageReceived = true;
// 0x16 is SSLv3/TLS "handshake" content type: https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record
this.isTLS = data[0] === 0x16;
if (this.isTLS)
this._attachTLSListeners();
else
this.target.on('data', data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data }));
}
if (this.isTLS)
this.internal!.push(data);
else
this.target.write(data);
}
private _attachTLSListeners() {
assert(this.isTLS);
this.internal = new SocksConnectionDuplex(data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data }));
this.internalTLS = new tls.TLSSocket(this.internal, {
isServer: true,
// openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout key.pem -out cert.pem -subj "/CN=localhost"
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
});
this.internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
const targetTLS = tls.connect({
socket: this.target,
rejectUnauthorized: this.socksProxy.contextOptions.ignoreHTTPSErrors === true ? false : true,
...clientCertificatesToTLSOptions(this.socksProxy.contextOptions.clientCertificates, `https://${this.host}:${this.port}/`),
});
this.internalTLS.pipe(targetTLS);
targetTLS.pipe(this.internalTLS);
// Handle close and errors
const closeBothSockets = () => {
this.internalTLS?.end();
targetTLS.end();
};
this.internalTLS.on('end', () => closeBothSockets());
targetTLS.on('end', () => closeBothSockets());
this.internalTLS.on('error', () => closeBothSockets());
targetTLS.on('error', error => {
if (['DEPTH_ZERO_SELF_SIGNED_CERT', 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'].includes(error.code)) {
this.internalTLS!.write('HTTP/1.1 503 Internal Server Error\r\n');
this.internalTLS!.write('Content-Type: text/html; charset=utf-8\r\n');
const responseBody = 'Self-signed certificate error: ' + error.message;
this.internalTLS!.write('Content-Length: ' + Buffer.byteLength(responseBody) + '\r\n');
this.internalTLS!.write('\r\n');
this.internalTLS!.write(responseBody);
this.internalTLS!.end();
return;
}
closeBothSockets();
});
}
}
export class ClientCertificatesProxy {
_socksProxy: SocksProxy;
private _connections: Map<string, SocksProxyConnection> = new Map();
constructor(
public readonly contextOptions: Pick<channels.BrowserNewContextOptions, 'clientCertificates' | 'ignoreHTTPSErrors'>
) {
this._socksProxy = new SocksProxy();
this._socksProxy.setPattern('*');
this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
try {
const connection = new SocksProxyConnection(this, payload.uid, payload.host, payload.port);
await connection.connect();
this._connections.set(payload.uid, connection);
} catch (error) {
this._socksProxy.socketFailed({ uid: payload.uid, errorCode: error.code });
}
});
this._socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => {
this._connections.get(payload.uid)?.onData(payload.data);
});
this._socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => {
this._connections.get(payload.uid)?.onClose();
this._connections.delete(payload.uid);
});
}
public async listen(): Promise<string> {
return `socks5://127.0.0.1:${await this._socksProxy.listen(0)}`;
}
public async close() {
await this._socksProxy.close();
}
}
export function clientCertificatesToTLSOptions(
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
requestURL: string
): Pick<https.RequestOptions, 'pfx' | 'key' | 'passphrase' | 'cert' | 'ca'> | undefined {
const matchingCerts = clientCertificates?.filter(c => urlMatches(undefined, requestURL, c.url));
if (!matchingCerts || !matchingCerts.length)
return;
const requestOptions = {
pfx: [] as { buf: Buffer, passphrase?: string }[],
key: [] as { pem: Buffer, passphrase?: string }[],
cert: [] as Buffer[],
};
for (const { certs } of matchingCerts) {
for (const cert of certs) {
if (cert.cert)
requestOptions.cert.push(cert.cert);
if (cert.key)
requestOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
if (cert.pfx)
requestOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
}
}
return requestOptions;
}
export function shouldUseMitmSocksProxy(options: {
ca?: Buffer[];
clientCertificates?: channels.BrowserNewContextOptions['clientCertificates'];
}) {
if (options.clientCertificates && options.clientCertificates.length > 0)
return true;
return false;
}

View file

@ -15590,6 +15590,45 @@ export interface APIRequest {
*/
baseURL?: string;
/**
* 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 `ca` property should be provided with the path to the
* file with the certificate authority's certificate. 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.
*/
clientCertificates?: Array<{
/**
* Glob pattern to match the URLs that the certificate is valid for.
*/
url: string;
/**
* List of client certificates to be used.
*/
certs: Array<{
/**
* Path to the file with the certificate in PEM format.
*/
cert?: string;
/**
* Path to the file with the private key in PEM format.
*/
key?: string;
/**
* Passphrase for the private key (PEM or PFX).
*/
passphrase?: string;
/**
* PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: string;
}>;
}>;
/**
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
@ -16741,6 +16780,45 @@ export interface Browser extends EventEmitter {
*/
bypassCSP?: boolean;
/**
* 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 `ca` property should be provided with the path to the
* file with the certificate authority's certificate. 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.
*/
clientCertificates?: Array<{
/**
* Glob pattern to match the URLs that the certificate is valid for.
*/
url: string;
/**
* List of client certificates to be used.
*/
certs: Array<{
/**
* Path to the file with the certificate in PEM format.
*/
cert?: string;
/**
* Path to the file with the private key in PEM format.
*/
key?: string;
/**
* Passphrase for the private key (PEM or PFX).
*/
passphrase?: string;
/**
* PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: string;
}>;
}>;
/**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
@ -20173,6 +20251,45 @@ export interface BrowserContextOptions {
*/
bypassCSP?: boolean;
/**
* 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 `ca` property should be provided with the path to the
* file with the certificate authority's certificate. 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.
*/
clientCertificates?: Array<{
/**
* Glob pattern to match the URLs that the certificate is valid for.
*/
url: string;
/**
* List of client certificates to be used.
*/
certs: Array<{
/**
* Path to the file with the certificate in PEM format.
*/
cert?: string;
/**
* Path to the file with the private key in PEM format.
*/
key?: string;
/**
* Passphrase for the private key (PEM or PFX).
*/
passphrase?: string;
/**
* PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: string;
}>;
}>;
/**
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.

View file

@ -4823,6 +4823,7 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
type BrowserName = 'chromium' | 'firefox' | 'webkit';
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
type ClientCertificate = Exclude<BrowserContextOptions['clientCertificates'], undefined>[0];
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
@ -5200,6 +5201,41 @@ export interface PlaywrightTestOptions {
*
*/
colorScheme: ColorScheme;
/**
* 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 `ca` property should be provided with the path to the
* file with the certificate authority's certificate. 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.
*
* **Usage**
*
* ```js
* // playwright.config.ts
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
* projects: [
* {
* name: 'Microsoft Edge',
* use: {
* ...devices['Desktop Edge'],
* clientCertificates: [{
* url: 'https://example.com',
* certs: [{
* cert: 'client/alice_cert.pem',
* key: 'client/alice_key.pem',
* passphase: 'mysecretpassword',
* }],
* }],
* },
* },
* ]
* });
* ```
*
*/
clientCertificates: ClientCertificate[];
/**
* Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about
* [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices).

View file

@ -576,6 +576,15 @@ export type PlaywrightNewRequestParams = {
userAgent?: string,
ignoreHTTPSErrors?: boolean,
extraHTTPHeaders?: NameValue[],
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
httpCredentials?: {
username: string,
password: string,
@ -600,6 +609,15 @@ export type PlaywrightNewRequestOptions = {
userAgent?: string,
ignoreHTTPSErrors?: boolean,
extraHTTPHeaders?: NameValue[],
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
httpCredentials?: {
username: string,
password: string,
@ -944,6 +962,15 @@ export type BrowserTypeLaunchPersistentContextParams = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -1017,6 +1044,15 @@ export type BrowserTypeLaunchPersistentContextOptions = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -1131,6 +1167,15 @@ export type BrowserNewContextParams = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -1190,6 +1235,15 @@ export type BrowserNewContextOptions = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -1252,6 +1306,15 @@ export type BrowserNewContextForReuseParams = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -1311,6 +1374,15 @@ export type BrowserNewContextForReuseOptions = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -4558,6 +4630,15 @@ export type AndroidDeviceLaunchBrowserParams = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
@ -4615,6 +4696,15 @@ export type AndroidDeviceLaunchBrowserOptions = {
height: number,
},
ignoreHTTPSErrors?: boolean,
clientCertificates?: {
url: string,
certs: {
cert?: Binary,
key?: Binary,
passphrase?: string,
pfx?: Binary,
}[],
}[],
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,

View file

@ -433,6 +433,21 @@ ContextOptions:
width: number
height: number
ignoreHTTPSErrors: boolean?
clientCertificates:
type: array?
items:
type: object
properties:
url: string
certs:
type: array
items:
type: object
properties:
cert: binary?
key: binary?
passphrase: string?
pfx: binary?
javaScriptEnabled: boolean?
bypassCSP: boolean?
userAgent: string?
@ -673,6 +688,21 @@ Playwright:
extraHTTPHeaders:
type: array?
items: NameValue
clientCertificates:
type: array?
items:
type: object
properties:
url: string
certs:
type: array
items:
type: object
properties:
cert: binary?
key: binary?
passphrase: string?
pfx: binary?
httpCredentials:
type: object?
properties:

View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFAzCCAuugAwIBAgIBATANBgkqhkiG9w0BAQsFADA2MRIwEAYDVQQDDAlsb2Nh
bGhvc3QxIDAeBgNVBAoMF0NsaWVudCBDZXJ0aWZpY2F0ZSBEZW1vMB4XDTI0MDYy
NDEyMzEyM1oXDTI1MDYyNDEyMzEyM1owEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0G
CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC4FGkFO0MTc2xgk8N9lrE8JW5MJQyx
me+h4HUqc+FwOddY3EH4akCKuvgmHP8bmB76yg7hsWiMHzNW4dqb+ZpxGaA1FInY
2gS/GHPaKmVH+lqtNeIAczu3gNo5yBEVp3GQmv0GGxwp5/ugOu+INfPtPqHEKdRm
+Ti7uXvMAeohh4raAv3hx3N2x+AnXpEa4ocz8fXYh4y3puk86KCA9Zq5O7IwB6QC
qBxSHdtamniqIzuevouXGd//ZCqjK8P57HqVZiBep/wByQlAN6WTRtjaPwPRT97A
YM0ayH91aaQ8BtqSWu2LoXkILV+SRSqLRqbyB4TUaKg0RaG4Fk04vg4xjOnjUNcZ
TEAKmAozQXTtazYqXGAgd5VENKk93wK1/B3zGnQBBmyyFsGbkCRZLPkxhXMIhoDc
ZcS4qUqVfA+gtv6+KlgiPBD4D5lGEKFPVV03T0v8+elq61+RsWHVa8VO3afFqFvT
i8yYwDS8udqGaE5EN7lyl1cYJwafH2mdiNdnQc4/Wp/zqgyPALjCbatyu3PAbTfK
DleGBRBfxiLKUB39Jo2bafClqRYQnCdQA1vQX0OF2Q9ZTrsLhskVsVQZN5C/Yopc
RsMpDWq7HPAP3WndVeawCBg6QFwnR0z6nmfi+UDz1hiSyl6bf3JJs3Nigll1W7UU
J56zv89nS8M4xwIDAQABo0IwQDAdBgNVHQ4EFgQUo4T4xMvLao//h+6AcFhXMe5l
iGkwHwYDVR0jBBgwFoAUCtaPQfMWTFHrjCUREWMGqIWYC/MwDQYJKoZIhvcNAQEL
BQADggIBABoH28SHV5P1xsrsBtEx8ab0BhSPHuzEI8ytA0KgwojrXRKX5MsLbLyY
9lFGH7fu+8830oDnfIe7989JMDGtccLO6DXWoWZjBVTkHPNjPSjF6TrhfqjeqNJH
NsPW9XUn3UB2hEYJ1nLLAzYizXYVSfL5HExF6Ph7fAzP6AvSZPS6AsVHXh2/JPvz
E/JzbeEewDsv3mB+DqUDne7Ukj/IIegGVx190KXwyLmgXlFd69R9GEVEO4FcXqpA
NCCcCmnTzBtzrgYVyWB7vDigMoD2bKWlQXwCgthR6DKywtKpSm9UWilghbgkuzam
toAOzIyZFGBt+44MnV/486XhbbIXzlixhSZPgSeOKM1tI69OOZ7ilvR6VxvPOrws
ZsUJDWFYYXq++8uzWRe+nHglXEQc+4CWbqqhjnncqh94/jLpHNS+BcrgDK3BuVrq
Skj8bNq8xnXgUPr+i9Rzmd4lRefKyjVehbRNXj4gp1Ap/TDQbl/A5Vdb31pTLYvO
SvDQXImm5HqV5KyddvKnWCIYVD7ATzcvjLqJl52ykDz4eRorzWQ41K5uwd0SxEyB
JZ0MesoWyEaWRlpjzqCNxCwO7Pp6RLMxDvC4/lkRt+7HxLLDWaw7bHplFm24GAYm
ebC5La1SdbYKC8AahYwT5Ppn6ThiSqV1m3GsuzgBLjfkYMFGjRQQ
-----END CERTIFICATE-----

View file

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEVTCCAj0CAQAwEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQC4FGkFO0MTc2xgk8N9lrE8JW5MJQyxme+h4HUqc+FwOddY
3EH4akCKuvgmHP8bmB76yg7hsWiMHzNW4dqb+ZpxGaA1FInY2gS/GHPaKmVH+lqt
NeIAczu3gNo5yBEVp3GQmv0GGxwp5/ugOu+INfPtPqHEKdRm+Ti7uXvMAeohh4ra
Av3hx3N2x+AnXpEa4ocz8fXYh4y3puk86KCA9Zq5O7IwB6QCqBxSHdtamniqIzue
vouXGd//ZCqjK8P57HqVZiBep/wByQlAN6WTRtjaPwPRT97AYM0ayH91aaQ8BtqS
Wu2LoXkILV+SRSqLRqbyB4TUaKg0RaG4Fk04vg4xjOnjUNcZTEAKmAozQXTtazYq
XGAgd5VENKk93wK1/B3zGnQBBmyyFsGbkCRZLPkxhXMIhoDcZcS4qUqVfA+gtv6+
KlgiPBD4D5lGEKFPVV03T0v8+elq61+RsWHVa8VO3afFqFvTi8yYwDS8udqGaE5E
N7lyl1cYJwafH2mdiNdnQc4/Wp/zqgyPALjCbatyu3PAbTfKDleGBRBfxiLKUB39
Jo2bafClqRYQnCdQA1vQX0OF2Q9ZTrsLhskVsVQZN5C/YopcRsMpDWq7HPAP3Wnd
VeawCBg6QFwnR0z6nmfi+UDz1hiSyl6bf3JJs3Nigll1W7UUJ56zv89nS8M4xwID
AQABoAAwDQYJKoZIhvcNAQELBQADggIBAG7XNJcmByEF0rvSV6bY28PrfirPt09K
dTsKNlv9a798k9eq/vqpnVrNslEFj/SVTBPl5r5FIYnueNiO54VlA6nJ+1yRSlvI
2SGvgCRoD4xNcMyJgzMwmxovhNRHdheRP+A82EUfgoT8/HCm0UasTw1PcZKokprb
T93pie4iV3CWBtVHd9/hZXsMnumT/LIbnUdCOkAsy7cIVsxOHNLZociJeV3LxIgL
B5HQdDHPd9i4C6zFgZmm7imrzvFrFk+ksUx1jUeMTXCCvwVyj5cPRbxKduFLCOIA
tP61RVeXt6ru5IZl+2n8GApKT9zkPhiGNmCdG54Z7Yc62fvijneeOlKPAwWRbxEq
smdW0fOaf73n0eguZ8ujk2ZLVDb2knqk5MoPTuEaSea/haVg/vgP8O5sFwjOcJ6P
D1flxGJafOGF0B0hnZXqFjUV2Ty+R5iSk7vaLiwc9asMybqyuEVv5q2/6gmoawt1
6srZq/Z3jyaseOzYgeOugZcKZyZNpDgS5ZIq+iKu5fP5gcnmvhZca4fFqd2jYthO
v6Xrd5tMcgpcZSRWAezh4p0AICnmdTlpQNcw6rdfr7RAT2OwxQmqW/SbniJCr/Nn
W4v6KhZ7mYBrYt7ywVXuhZKmubwShZTprSEsQko5Tujk41/kLJl0C3kIdkMvMWzF
gKrkAP6tuQQ7
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
XJVwXoZ9PUMxrfgEI9YmZdHOSJdU9Qc=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIEyzCCArOgAwIBAgIUBO3H8U57HcsnWmwP2Xm7wDgHnmkwDQYJKoZIhvcNAQEL
BQAwDjEMMAoGA1UEAwwDQm9iMB4XDTI0MDYyNDEyMzEyOVoXDTI1MDYyNDEyMzEy
OVowDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEAugNWP3UMpf33fOykU7vrIMv1LS32KZjloQnW6o6t5kjt/Yr3JDKnEuMMshpC
4YGTEBJ0y5errBbCP3bQY62SnARAgXAaatRqGF+rqiKEnTDCXQo6ex27nna2LgjU
JjpU7uqW3fIbECUDIG2zXpE+9VJ1CWViGrRFN7M57zoJ4SEutyMSu9Z8qeZ+/sHm
UjqLgimjxx5KSLe6wx9BI7266cgVLbGLMEzZYvDz3+TznzcfTiUs0h74LBDlQYjh
76f2td3gZZ7jii2toJgV+E72tjIOANkLVtIVgx0le/4dgwgoIEkjTm5bvbTijIuh
O01TYjoV+r+aeTnl+uGGLGTG9KtMj+HOpDxAapFxLS8sM2kn9bBu+6XdP2sa89PX
7AmZ1VjsMFILM20kvbOmfnKrVKTszo68LBeMIcYfR60KaPc/ZglvHFE7lxB4rOBz
4QNCNMDciEh6npkntjGnO6q01DhYdBpccW65mAqX0+LrkjpUcBneYoTyd0Izd5M1
QNm6WaL2Uye+wmNBIptq8I0A6PiWdVbef3xcF70I9JTJHFODHjs1PDov/llZpUyh
cNx8WC+y/nPdt+XS0BR/ap0QWJ66CNa5tsLs6txdx6Aoa373bq6HlY/RRlvkhPxr
OOKfon7qKQCQGPTSuSvw8qssehcko79chpaYTPJKaJZRK0sCAwEAAaMhMB8wHQYD
VR0OBBYEFBcJPRVNgMzwnvzLnNpsVlQySkWpMA0GCSqGSIb3DQEBCwUAA4ICAQAu
KjNiFaTPS4vRwva/kNY5MUBcTH0U3BGPOlWyvrudc3FM8+X7OhGpBLLAGmk/H+KT
Kzd2B3btr1AAxprLMehxXVlSF0+5A2DLyNDq2wwoI+V2APpeeGA4cCRsL91ZqCH8
57T/XpRWKKorlY17yfxO9GUFJGdl1Oki3wvOOaXWcuxUb7nHOjI7oguUcR4jfdIR
WfeUe3P+Y8TLVe2WRdJYEdRfpKN2T+8dGNdVHJ3GokgoLQsj8wpKHM0cd81f+mMf
jKfP++mR9w+UCRKgWxbCTFMhZvz4BKwpmLI0mLphumiWkfFJPrxTUmx/0JFfOLdi
pDHqd5JfSeiBm+hKTWlY/kc7rPSe+VYiXM+Zs+4EIqjowjiRixW+lRU4lf+7ZDlm
v+mi4C4JLHW6I7H085GlM+A6BmEPPBRNx2OOPEIqqLCRkJMi3RmS6X6jkAcCdhmn
MsxEKjgG8dJn+kfDGxD1Vfz5PjqzFhHPyahdkXo2br8P3RH7jPY9lb6nvAKupHlV
GgKJodeibtbZl1eeCrwdHjrawmQ7VhyQ91Dk7PP+z9h6fQqFxxULYP6WC0P7KVfD
EBauab6AJdpwKPqsRL9w87yjl2481aeMdtezYjE1HuWTlObq9YcArbpE8Rm5gLb1
9bZ5aZKz62m+O3LZMbsMDusgTeyZFMlB0yA32q+SGg==
-----END CERTIFICATE-----

View file

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC
Ag8AMIICCgKCAgEAugNWP3UMpf33fOykU7vrIMv1LS32KZjloQnW6o6t5kjt/Yr3
JDKnEuMMshpC4YGTEBJ0y5errBbCP3bQY62SnARAgXAaatRqGF+rqiKEnTDCXQo6
ex27nna2LgjUJjpU7uqW3fIbECUDIG2zXpE+9VJ1CWViGrRFN7M57zoJ4SEutyMS
u9Z8qeZ+/sHmUjqLgimjxx5KSLe6wx9BI7266cgVLbGLMEzZYvDz3+TznzcfTiUs
0h74LBDlQYjh76f2td3gZZ7jii2toJgV+E72tjIOANkLVtIVgx0le/4dgwgoIEkj
Tm5bvbTijIuhO01TYjoV+r+aeTnl+uGGLGTG9KtMj+HOpDxAapFxLS8sM2kn9bBu
+6XdP2sa89PX7AmZ1VjsMFILM20kvbOmfnKrVKTszo68LBeMIcYfR60KaPc/Zglv
HFE7lxB4rOBz4QNCNMDciEh6npkntjGnO6q01DhYdBpccW65mAqX0+LrkjpUcBne
YoTyd0Izd5M1QNm6WaL2Uye+wmNBIptq8I0A6PiWdVbef3xcF70I9JTJHFODHjs1
PDov/llZpUyhcNx8WC+y/nPdt+XS0BR/ap0QWJ66CNa5tsLs6txdx6Aoa373bq6H
lY/RRlvkhPxrOOKfon7qKQCQGPTSuSvw8qssehcko79chpaYTPJKaJZRK0sCAwEA
AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQAFGvffXDPy6TKwrdQShBTCd2sH6lTW7K3D
2AzL8ZWLC20QmQtz3YbyDODnauo/8EKGvCpvTHZZoPQbmKgFro9vbLMp++2npvY8
9VD7FENkfnlyYspiaLicmmV+wN8MwDgKhnZYM41GnkxUrDCj8iOmFK3bmvvhOD1H
1SCWhnWG6VdaWhIbE0faXzK7+0WhHILaWxZTgVAHKavQ3APYEh2+s1UwseugNBsL
1p+ldROFK/SIisyzi009a24/Ccan9peJbmmWKKUF7oGqoIYfoeDMPpCG+rWxzpaG
1S4DNAVgupLpX/oapzBZs3+mXCfh9NSkjKW0Z+M2yO0qOrhdnO2tdpQ2693JoxsG
mFhNtfno+22pBHUY6w9jIFIxNbNMWlS54fZoCg7pgJ0YmME7zuoeu2IUeDb6MSoE
fd4S+iGdIhyb83yafe9ws+6u7Es7/ivrU2E5E9dtae11liuGQvYxqIpR5ArGndwr
Kv3czIKmkR6R8a7UPRXRona5do8L4uhBYz8SnEaH3ClW80NttDO5F3geHM+znTCS
uc3ruF3fl4h6rh7khDBY7c62jA1F8f+plAgjrCAI6ZSyNUdAMpIzwayGE+i/7M92
ivWpiPd4neB3ZSP3T6fNlz2SjnmTMwvGHcqvnYAohdqMqrn9MzA4lLX+/Qg+CeRS
m3dDmOG0Kw==
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
n8nUUVeSi2z5bdctVUaQjQqc6zKI
-----END PRIVATE KEY-----

View file

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFTTCCAzWgAwIBAgIUWdMb2scNR1R1TxDuI4RrFt7mGOwwDQYJKoZIhvcNAQEL
BQAwNjESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYDVQQKDBdDbGllbnQgQ2VydGlm
aWNhdGUgRGVtbzAeFw0yNDA2MjQxMjMxMTNaFw0yNTA2MjQxMjMxMTNaMDYxEjAQ
BgNVBAMMCWxvY2FsaG9zdDEgMB4GA1UECgwXQ2xpZW50IENlcnRpZmljYXRlIERl
bW8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9VMWaGggaYorXO5IF
qj3s+W4nvrzzi3LHYjnugFnUAruOYdXbBboieSR2wnJeITyVp0MbVEsrcfwsCmlB
RP3ehheJqW2GtDZ8eFTzZ/r2Npl/TQgGn5/R+HYe9NkXfWAT+VanURcNrP98eSRI
zDS/Rx5ZgG9/iYRbB/Lcd+Stkp2UTLDlMTvAv8CFwFLE1bwHmcAqYBMMqEpKh3Z5
oshmOSQz28A5o3UK2XCJptywV0lbugs0bBSQiayBs2BzMLbUFiWyh45LEvuWLYpv
3FKQwMJi+ZBZ0lOXjFOfW0TXlZRLMSrSfnOP0dq8x2QAFEIIKGwgbZXyCLP+BVLD
IJ39+AadSjXaJ+12FkOE8ETe6EEIKoydwzFnxtndMvflKYFILRF8vJp44MTJ/cZ2
9ZSY7QPVoSEK9KjtedlmYSvyWYYxtoWN5zR/K7Oyb/lFy45oKU75LuWt5qx9X2eA
uScy8SWgo17SN1IF1OdOYjExRXmjnHcQJIAPLfX6hkheee9S8uiWy8a42iBJW4ZV
QSsGtbMb3BojjykZYkhTdjmjvjy16IqlJ4JEcRGKxuuvmlCXOs33D1ot7xb1GHwX
oLvXo2IOSZAjDOSHCOFdvg9h0LcyEeGTeLHDeNrxRUo1z+/zRU3NE8A4UDyKP/rf
ZTVphGSNoyvo0UwyIxFQutZK6QIDAQABo1MwUTAdBgNVHQ4EFgQUCtaPQfMWTFHr
jCUREWMGqIWYC/MwHwYDVR0jBBgwFoAUCtaPQfMWTFHrjCUREWMGqIWYC/MwDwYD
VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEANgmi2fMKEFlIyKCz+4Oa
GnOKIAACR0+yk2G+nrGxkkmT2bIiibcbYtLmfB3gQyA6O3qoi94t66QS83fIvRA5
FHRs/wWGzKHcDCaJApxVhIN7V+480NB2W7VS2KSQh9EQQ9dziXS6xftycz7LeLR0
WYcvojLCd1tkCFFnaN0cRjqjMdfGfsONtvc5KFGoySNs3r65ckQct7e34BELMBjv
Zd+myNNnw6i8cnHFcnJpSYajX3dJrmaMtapjORjTjlpsjF7oYOx2dANHzOJ8sVqo
3cElK/Ou88S/EHX/xOXx/jncz2OPGGL/8UEDh9Z2Co4/p1TYaxMHOBTlWlqjERWZ
AmA9v9lU6gn+o5ITz0wm3M+StxF3DU9nCu3/FCezObNPM1sILRZoYmx8Ok85G/0l
DeYgxE4je4CCh1rxyRZClRljbimE/FBbw/Ui6zrHiTAABfwVirY+wyDvPMiYhGmt
sOfEveXVn5aX7sW1iOChZP3p2KuL5a19T5sl7+mev6Z1sea3IovYeXKsRFEVhTUU
7533qt9lknpip9I27BT8aJKWUGP3GWXDs/DVoSkygnvbDDJfLK5Wqp8EPZCLyS1K
L/lWamgel5BgCwb091P2rNT7Lu+XGhJOcfyFk1FAwiZ30yi3As4W9HXvdh3CNm5x
55g7Z6Xmxn6xdtviPZmBPZU=
-----END CERTIFICATE-----

View file

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFTTCCAzWgAwIBAgIUTivXxf4wmYQ2SLCYkHRAIa1oI/kwDQYJKoZIhvcNAQEL
BQAwNjESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYDVQQKDBdDbGllbnQgQ2VydGlm
aWNhdGUgRGVtbzAeFw0yNDA2MjQxMjUyMDlaFw0yNTA2MjQxMjUyMDlaMDYxEjAQ
BgNVBAMMCWxvY2FsaG9zdDEgMB4GA1UECgwXQ2xpZW50IENlcnRpZmljYXRlIERl
bW8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDHVOjEeKWwo0+X5ZDV
bSVAHUDklVvcCczZWtIc3DOEU84Gc+JP/ug1AhgEAS4LbIMEpyIyzBcm0mMrLT7i
02ci7J3ybIlaIn2gNQ703WaJ2hWQ+GzubFKDCZmiiabCZy/uYyJ9D0EWZ6gK64xx
gWXiiud8Lbje73RwjquKCKDTH8kd4s99taK4UmpXa6ERVUTCbIOCq0rNvJTQYCf6
XDVZeucnW4CceOyIbeYA0hbBA+WF4BuyC0rDYdyQLNPQPEMwo/thLc9VhEMiL0B0
QHXvVNO3T0OSVcdgogjTpbAk6thKHjKjeqrwtFaU4pmyT68yxPk6NCKgVfMQTRt2
5o5kCG3QyoFAWA0jMz+IGtwtjbn0wVFhVfRlX/pUQHrdxn8o+S+ynbM87b7DHEO/
/RiMAropa4jcVWe0PC+1vJm7bXm1aLt0vUrde8lzYNN1C1iEVbF/yPvGBysxbFrh
Ec4Temuj7B9vFieKf5hw7+lEzVteABUUee+zhmR8lunQz4fa09QB59ot2Z//IZyF
+uduZZjYqV6YAzlulXzXJb+LAdpWVWXZzwq+ORhh0cExkZ0R72qrB7cj3qodR03t
yYCddmK2/Iafw7HoN7Wa0aVFw81NSEyFKwL/Mxs+QQy0jkRzBPCTh8NzdWBA40Qo
ZPjrfBZo3bHAzMFJ95MN2p04OQIDAQABo1MwUTAdBgNVHQ4EFgQUpGMrYBCb6V6L
gjM7kN+rcFqcCYcwHwYDVR0jBBgwFoAUpGMrYBCb6V6LgjM7kN+rcFqcCYcwDwYD
VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAPY42mRjSrkeQBhfINNy+
W2S+1XIkxVcNyczt4xsxErUesrlStHFthv1n4b2RZbbAREQWNm7d06B0FvQ1/XBn
NAvVKCN6gf/jO+BYv8kn/8TniWVmrlEn9Y3sX8H0bqReYKQD9YmroO8vbHFYpDpY
vOiS1J0S5nePXz1VvhQ7VG3V/OP6jGT1NqDZU39AlbA8NN1WiftB6JxK9UvFQnCf
coG8maKoDtlf4bXTbZMbsF80ocDJLWq0BJjnKy7I79qOr66mt+nqB93v/2zTIToP
FD9Uw3lX+Vseyya3G5AqZZJj2suL/TZx3RkicGZyz/5v7dB0YuX/yjvKtdAif9Lj
w/kaLNeICuiolwSia6Ss1V8IGMYyFoRIKcQyA1pWmSYRbP2b2ZVctK/zGgt2ydHG
OUSENBoUykU3z8k+jr6cNKPwZJBITAD1vN2M6GFfET4R1iw55gcc2D5Tm8dkIHSq
WXoLDpofnABkaowaD5Xe6tySCoHWxpkIDSx8mse/sM8S15azDhL7V+IWWequ7i2p
oG5q19E3uy3IoANZdBzCZoYTcjv/CmTSJMOTMk4MaT4JmtGzMGNWwmr6dVOR1c21
FuzKN5liY6qRzYgOisPCUwkYXkJelfwup3nK0g3jBI9S8lB2ucfB15dRmwVjGP1G
ySv953S5j/5ZLvJmGkahPTw=
-----END CERTIFICATE-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
m3Ea0Y3lf0rJCBJ10pFBP3z8orJyWkI=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
3bsZaDP7F6iGp+Kh73PM8Yxe9Pyc
-----END PRIVATE KEY-----

View file

@ -0,0 +1,252 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import { expect, playwrightTest as base } from '../config/browserTest';
import https from 'https';
import type net from 'net';
const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: string }>({
serverURL: async ({ }, use) => {
const server = https.createServer({
key: fs.readFileSync(path.join(kClientCertificatesDir, 'server/server_key.pem')),
cert: fs.readFileSync(path.join(kClientCertificatesDir, 'server/server_cert.pem')),
ca: [
fs.readFileSync(path.join(kClientCertificatesDir, 'server/server_cert.pem')),
],
requestCert: true,
rejectUnauthorized: false,
}, (req, res) => {
const cert = (req.socket as import('tls').TLSSocket).getPeerCertificate();
if ((req as any).client.authorized) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
} else if (cert.subject) {
res.writeHead(403, { 'Content-Type': 'text/html' });
res.end(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
} else {
res.writeHead(401, { 'Content-Type': 'text/html' });
res.end(`Sorry, but you need to provide a client certificate to continue.`);
}
});
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
await use(`https://localhost:${(server.address() as net.AddressInfo).port}/`);
await new Promise<void>(resolve => server.close(() => resolve()));
},
serverURLRewrittenToLocalhost: async ({ serverURL, browserName }, use) => {
const parsed = new URL(serverURL);
parsed.hostname = 'i-get-rewritten-to-localhost-on-the-server-side';
const shouldRewriteToLocalhost = browserName === 'webkit' && process.platform === 'darwin';
await use(shouldRewriteToLocalhost ? parsed.toString() : serverURL);
}
});
test.skip(({ mode }) => mode !== 'default');
const kClientCertificatesDir = path.join(__dirname, '../config/testserver/client-certificates');
const kValidationSubTests: any[] = [
[[{ url: 'test', certs: [] }], 'No certs specified for url: test'],
[[{ url: 'test', certs: [{}] }]], 'None of cert, key, passphrase or pfx is specified',
[[{
url: 'test', certs: [{
cert: 'foo',
key: 'foo',
passphrase: 'foo',
pfx: 'foo',
}]
}], 'pfx is specified together with cert, key or passphrase']
];
test.describe('fetch', () => {
test('validate input', async ({ playwright }) => {
for (const [clientCertificates, expected] of kValidationSubTests)
await expect(playwright.request.newContext({ clientCertificates })).rejects.toThrow(expected);
});
test('should fail with no client certificates provided', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
const response = await request.get(serverURL);
expect(response.status()).toBe(401);
expect(await response.text()).toBe('Sorry, but you need to provide a client certificate to continue.');
await request.dispose();
});
test('should keep supporting http', async ({ playwright, server }) => {
const request = await playwright.request.newContext({
clientCertificates: [{
url: server.PREFIX,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
const response = await request.get(server.PREFIX + '/one-style.html');
expect(response.url()).toBe(server.PREFIX + '/one-style.html');
expect(response.status()).toBe(200);
expect(await response.text()).toContain('<div>hello, world!</div>');
await request.dispose();
});
test('should throw with a untrusted CA', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
try {
const response = await request.get(serverURL);
expect(response.status()).toBe(503);
expect(await response.text()).toBe('Self-signed certificate error: self-signed certificate');
} catch (error) {
expect(error.message).toContain('self-signed certificate');
}
});
test('should throw with matching CA but untrusted client certs', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/bob_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/bob_key.pem'),
}],
}],
});
const response = await request.get(serverURL);
expect(response.url()).toBe(serverURL);
expect(response.status()).toBe(403);
expect(await response.text()).toBe('Sorry Bob, certificates from Bob are not welcome here.');
await request.dispose();
});
test('should work without CA', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
ignoreHTTPSErrors: true,
});
const response = await request.get(serverURL);
expect(response.url()).toBe(serverURL);
expect(response.status()).toBe(200);
expect(await response.text()).toBe('Hello Alice, your certificate was issued by localhost!');
await request.dispose();
});
test('should work in the browser with request interception', async ({ browser, playwright, serverURL }) => {
const request = await playwright.request.newContext({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
const page = await browser.newPage({ ignoreHTTPSErrors: true });
await page.route('**/*', async route => {
const response = await request.fetch(route.request());
await route.fulfill({ response });
});
await page.goto(serverURL);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await page.close();
await request.dispose();
});
});
test.describe('browser', () => {
test('validate input', async ({ browser }) => {
for (const [clientCertificates, expected] of kValidationSubTests)
await expect(browser.newContext({ clientCertificates })).rejects.toThrow(expected);
});
test('should keep supporting http', async ({ browser, server }) => {
const page = await browser.newPage({
clientCertificates: [{
url: server.PREFIX,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
await page.goto(server.PREFIX + '/one-style.html');
await expect(page.getByText('hello, world!')).toBeVisible();
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
await page.close();
});
test('should fail with no client certificates', async ({ browser, serverURLRewrittenToLocalhost }) => {
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: 'https://not-matching.com',
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
await page.goto(serverURLRewrittenToLocalhost);
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
await page.close();
});
test('should fail with untrusted client certs', async ({ browser, serverURLRewrittenToLocalhost }) => {
const page = await browser.newPage({
clientCertificates: [{
url: serverURLRewrittenToLocalhost,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/bob_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/bob_key.pem'),
}],
}],
});
await page.goto(serverURLRewrittenToLocalhost);
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
await page.close();
});
test('should pass with matching certificates', async ({ browser, serverURLRewrittenToLocalhost }) => {
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: serverURLRewrittenToLocalhost,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
await page.goto(serverURLRewrittenToLocalhost);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await page.close();
});
});

View file

@ -152,6 +152,7 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
type BrowserName = 'chromium' | 'firefox' | 'webkit';
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
type ClientCertificate = Exclude<BrowserContextOptions['clientCertificates'], undefined>[0];
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
@ -209,6 +210,7 @@ export interface PlaywrightTestOptions {
acceptDownloads: boolean;
bypassCSP: boolean;
colorScheme: ColorScheme;
clientCertificates: ClientCertificate[];
deviceScaleFactor: number | undefined;
extraHTTPHeaders: ExtraHTTPHeaders | undefined;
geolocation: Geolocation | undefined;