mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: support client certificates (#31529)
Signed-off-by: Max Schmitt <max@schmitt.mx> Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
This commit is contained in:
parent
229000501e
commit
9569cb5c1e
@ -12,6 +12,9 @@ see [APIRequestContext].
|
|||||||
|
|
||||||
Creates new instances of [APIRequestContext].
|
Creates new instances of [APIRequestContext].
|
||||||
|
|
||||||
|
### option: APIRequest.newContext.clientCertificates = %%-context-option-clientCertificates-%%
|
||||||
|
* since: 1.46
|
||||||
|
|
||||||
### option: APIRequest.newContext.useragent = %%-context-option-useragent-%%
|
### option: APIRequest.newContext.useragent = %%-context-option-useragent-%%
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
|
|
||||||
|
@ -256,6 +256,9 @@ await browser.CloseAsync();
|
|||||||
### option: Browser.newContext.proxy = %%-context-option-proxy-%%
|
### option: Browser.newContext.proxy = %%-context-option-proxy-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
|
### option: Browser.newContext.clientCertificates = %%-context-option-clientCertificates-%%
|
||||||
|
* since: 1.46
|
||||||
|
|
||||||
### option: Browser.newContext.storageState = %%-js-python-context-option-storage-state-%%
|
### option: Browser.newContext.storageState = %%-js-python-context-option-storage-state-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
@ -281,6 +284,9 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
|
|||||||
### option: Browser.newPage.proxy = %%-context-option-proxy-%%
|
### option: Browser.newPage.proxy = %%-context-option-proxy-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
|
### option: Browser.newPage.clientCertificates = %%-context-option-clientCertificates-%%
|
||||||
|
* since: 1.46
|
||||||
|
|
||||||
### option: Browser.newPage.storageState = %%-js-python-context-option-storage-state-%%
|
### option: Browser.newPage.storageState = %%-js-python-context-option-storage-state-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
|
@ -343,6 +343,9 @@ use a temporary directory instead.
|
|||||||
### option: BrowserType.launchPersistentContext.firefoxUserPrefs2 = %%-csharp-java-browser-option-firefoxuserprefs-%%
|
### option: BrowserType.launchPersistentContext.firefoxUserPrefs2 = %%-csharp-java-browser-option-firefoxuserprefs-%%
|
||||||
* since: v1.40
|
* since: v1.40
|
||||||
|
|
||||||
|
### option: BrowserType.launchPersistentContext.clientCertificates = %%-context-option-clientCertificates-%%
|
||||||
|
* since: 1.46
|
||||||
|
|
||||||
## async method: BrowserType.launchServer
|
## async method: BrowserType.launchServer
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: js
|
* langs: js
|
||||||
|
@ -514,6 +514,25 @@ 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.
|
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.
|
||||||
|
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
|
||||||
|
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
|
||||||
|
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
|
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
|
||||||
|
|
||||||
|
An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the private key is encrypted. 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.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::note
|
||||||
|
When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`.
|
||||||
|
:::
|
||||||
|
|
||||||
## context-option-useragent
|
## context-option-useragent
|
||||||
- `userAgent` <[string]>
|
- `userAgent` <[string]>
|
||||||
|
|
||||||
|
@ -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: [{
|
||||||
|
certPath: './cert.pem',
|
||||||
|
keyPath: './key.pem',
|
||||||
|
passphase: 'mysecretpassword',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## property: TestOptions.colorScheme = %%-context-option-colorscheme-%%
|
## property: TestOptions.colorScheme = %%-context-option-colorscheme-%%
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
|
|
||||||
|
11
packages/playwright-core/bin/socks-certs/README.md
Normal file
11
packages/playwright-core/bin/socks-certs/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Certfificates for Socks Proxy
|
||||||
|
|
||||||
|
These certificates are used when client certificates are used with
|
||||||
|
Playwright. Playwright then creates a Socks proxy, which sits between
|
||||||
|
the browser and the actual target server. The Socks proxy uses this certificiate
|
||||||
|
to talk to the browser and establishes its own secure TLS connection to the server.
|
||||||
|
The certificates are generated via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout key.pem -out cert.pem -subj "/CN=localhost"
|
||||||
|
```
|
19
packages/playwright-core/bin/socks-certs/cert.pem
Normal file
19
packages/playwright-core/bin/socks-certs/cert.pem
Normal 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-----
|
28
packages/playwright-core/bin/socks-certs/key.pem
Normal file
28
packages/playwright-core/bin/socks-certs/key.pem
Normal 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-----
|
@ -529,6 +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.clientCertificates),
|
||||||
};
|
};
|
||||||
if (!contextParams.recordVideo && options.videosPath) {
|
if (!contextParams.recordVideo && options.videosPath) {
|
||||||
contextParams.recordVideo = {
|
contextParams.recordVideo = {
|
||||||
@ -548,3 +549,21 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
|
|||||||
return 'accept';
|
return 'accept';
|
||||||
return 'deny';
|
return 'deny';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function toClientCertificatesProtocol(clientCertificates?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||||
|
if (!clientCertificates)
|
||||||
|
return undefined;
|
||||||
|
return await Promise.all(clientCertificates.map(async clientCertificate => {
|
||||||
|
return {
|
||||||
|
url: clientCertificate.url,
|
||||||
|
certs: await Promise.all(clientCertificate.certs.map(async cert => {
|
||||||
|
return {
|
||||||
|
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
|
||||||
|
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
|
||||||
|
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
|
||||||
|
passphrase: cert.passphrase,
|
||||||
|
};
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
@ -25,10 +25,11 @@ import { assert, headersObjectToArray, isString } from '../utils';
|
|||||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import { RawHeaders } from './network';
|
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 type { Playwright } from './playwright';
|
||||||
import { Tracing } from './tracing';
|
import { Tracing } from './tracing';
|
||||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||||
|
import { toClientCertificatesProtocol } from './browserContext';
|
||||||
|
|
||||||
export type FetchOptions = {
|
export type FetchOptions = {
|
||||||
params?: { [key: string]: string; },
|
params?: { [key: string]: string; },
|
||||||
@ -44,9 +45,10 @@ export type FetchOptions = {
|
|||||||
maxRetries?: number,
|
maxRetries?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'storageState' | 'tracesDir'> & {
|
type NewContextOptions = Omit<channels.PlaywrightNewRequestOptions, 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'tracesDir'> & {
|
||||||
extraHTTPHeaders?: Headers,
|
extraHTTPHeaders?: Headers,
|
||||||
storageState?: string | StorageState,
|
storageState?: string | StorageState,
|
||||||
|
clientCertificates?: ClientCertificate[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RequestWithBodyOptions = Omit<FetchOptions, 'method'>;
|
type RequestWithBodyOptions = Omit<FetchOptions, 'method'>;
|
||||||
@ -74,6 +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.clientCertificates),
|
||||||
})).request);
|
})).request);
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._request = this;
|
context._request = this;
|
||||||
@ -175,7 +178,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||||||
const params = objectToArray(options.params);
|
const params = objectToArray(options.params);
|
||||||
const method = options.method || options.request?.method();
|
const method = options.method || options.request?.method();
|
||||||
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
// 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;
|
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
||||||
let jsonData: any;
|
let jsonData: any;
|
||||||
let formData: channels.NameValue[] | undefined;
|
let formData: channels.NameValue[] | undefined;
|
||||||
|
@ -47,7 +47,17 @@ export type SetStorageState = {
|
|||||||
export type LifecycleEvent = channels.LifecycleEvent;
|
export type LifecycleEvent = channels.LifecycleEvent;
|
||||||
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
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: {
|
||||||
|
certPath?: string;
|
||||||
|
keyPath?: string;
|
||||||
|
pfxPath?: string;
|
||||||
|
passphrase?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads'> & {
|
||||||
viewport?: Size | null;
|
viewport?: Size | null;
|
||||||
extraHTTPHeaders?: Headers;
|
extraHTTPHeaders?: Headers;
|
||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
@ -70,6 +80,7 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
|
|||||||
reducedMotion?: 'reduce' | 'no-preference' | null;
|
reducedMotion?: 'reduce' | 'no-preference' | null;
|
||||||
forcedColors?: 'active' | 'none' | null;
|
forcedColors?: 'active' | 'none' | null;
|
||||||
acceptDownloads?: boolean;
|
acceptDownloads?: boolean;
|
||||||
|
clientCertificates?: ClientCertificate[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type LaunchOverrides = {
|
type LaunchOverrides = {
|
||||||
|
@ -331,6 +331,15 @@ scheme.PlaywrightNewRequestParams = tObject({
|
|||||||
userAgent: tOptional(tString),
|
userAgent: tOptional(tString),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
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({
|
httpCredentials: tOptional(tObject({
|
||||||
username: tString,
|
username: tString,
|
||||||
password: tString,
|
password: tString,
|
||||||
@ -532,6 +541,15 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
|
|||||||
height: tNumber,
|
height: tNumber,
|
||||||
})),
|
})),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
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),
|
javaScriptEnabled: tOptional(tBoolean),
|
||||||
bypassCSP: tOptional(tBoolean),
|
bypassCSP: tOptional(tBoolean),
|
||||||
userAgent: tOptional(tString),
|
userAgent: tOptional(tString),
|
||||||
@ -614,6 +632,15 @@ scheme.BrowserNewContextParams = tObject({
|
|||||||
height: tNumber,
|
height: tNumber,
|
||||||
})),
|
})),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
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),
|
javaScriptEnabled: tOptional(tBoolean),
|
||||||
bypassCSP: tOptional(tBoolean),
|
bypassCSP: tOptional(tBoolean),
|
||||||
userAgent: tOptional(tString),
|
userAgent: tOptional(tString),
|
||||||
@ -676,6 +703,15 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
|||||||
height: tNumber,
|
height: tNumber,
|
||||||
})),
|
})),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
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),
|
javaScriptEnabled: tOptional(tBoolean),
|
||||||
bypassCSP: tOptional(tBoolean),
|
bypassCSP: tOptional(tBoolean),
|
||||||
userAgent: tOptional(tString),
|
userAgent: tOptional(tString),
|
||||||
@ -2513,6 +2549,15 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
|||||||
height: tNumber,
|
height: tNumber,
|
||||||
})),
|
})),
|
||||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
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),
|
javaScriptEnabled: tOptional(tBoolean),
|
||||||
bypassCSP: tOptional(tBoolean),
|
bypassCSP: tOptional(tBoolean),
|
||||||
userAgent: tOptional(tString),
|
userAgent: tOptional(tString),
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { BrowserContext, validateBrowserContextOptions } from './browserContext';
|
import { BrowserContext, createClientCertificatesProxyIfNeeded, validateBrowserContextOptions } from './browserContext';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
import type { ProxySettings } from './types';
|
import type { ProxySettings } from './types';
|
||||||
@ -84,7 +84,15 @@ export abstract class Browser extends SdkObject {
|
|||||||
|
|
||||||
async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||||
validateBrowserContextOptions(options, this.options);
|
validateBrowserContextOptions(options, this.options);
|
||||||
const context = await this.doCreateNewContext(options);
|
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options);
|
||||||
|
let context;
|
||||||
|
try {
|
||||||
|
context = await this.doCreateNewContext(options);
|
||||||
|
} catch (error) {
|
||||||
|
await clientCertificatesProxy?.close();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
context._clientCertificatesProxy = clientCertificatesProxy;
|
||||||
if (options.storageState)
|
if (options.storageState)
|
||||||
await context.setStorageState(metadata, options.storageState);
|
await context.setStorageState(metadata, options.storageState);
|
||||||
return context;
|
return context;
|
||||||
|
@ -43,6 +43,7 @@ import * as consoleApiSource from '../generated/consoleApiSource';
|
|||||||
import { BrowserContextAPIRequestContext } from './fetch';
|
import { BrowserContextAPIRequestContext } from './fetch';
|
||||||
import type { Artifact } from './artifact';
|
import type { Artifact } from './artifact';
|
||||||
import { Clock } from './clock';
|
import { Clock } from './clock';
|
||||||
|
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
||||||
|
|
||||||
export abstract class BrowserContext extends SdkObject {
|
export abstract class BrowserContext extends SdkObject {
|
||||||
static Events = {
|
static Events = {
|
||||||
@ -90,6 +91,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||||||
private _debugger!: Debugger;
|
private _debugger!: Debugger;
|
||||||
_closeReason: string | undefined;
|
_closeReason: string | undefined;
|
||||||
readonly clock: Clock;
|
readonly clock: Clock;
|
||||||
|
_clientCertificatesProxy: ClientCertificatesProxy | undefined;
|
||||||
|
|
||||||
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
|
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
|
||||||
super(browser, 'browser-context');
|
super(browser, 'browser-context');
|
||||||
@ -245,6 +247,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||||||
// at the same time.
|
// at the same time.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._clientCertificatesProxy?.close().catch(() => {});
|
||||||
this.tracing.abort();
|
this.tracing.abort();
|
||||||
if (this._isPersistentContext)
|
if (this._isPersistentContext)
|
||||||
this.onClosePersistent();
|
this.onClosePersistent();
|
||||||
@ -655,6 +658,18 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createClientCertificatesProxyIfNeeded(options: channels.BrowserNewContextOptions, browserOptions?: BrowserOptions) {
|
||||||
|
if (!options.clientCertificates?.length)
|
||||||
|
return;
|
||||||
|
if (options.proxy?.server || browserOptions?.proxy?.server)
|
||||||
|
throw new Error('Cannot specify both proxy and clientCertificates');
|
||||||
|
verifyClientCertificates(options.clientCertificates);
|
||||||
|
const clientCertificatesProxy = new ClientCertificatesProxy(options);
|
||||||
|
options.proxy = { server: await clientCertificatesProxy.listen() };
|
||||||
|
options.ignoreHTTPSErrors = true;
|
||||||
|
return clientCertificatesProxy;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) {
|
export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) {
|
||||||
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
||||||
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
||||||
@ -707,6 +722,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))
|
||||||
|
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;
|
||||||
|
@ -18,7 +18,7 @@ import fs from 'fs';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { BrowserContext } from './browserContext';
|
import type { BrowserContext } from './browserContext';
|
||||||
import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
|
import { createClientCertificatesProxyIfNeeded, normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
|
||||||
import type { BrowserName } from './registry';
|
import type { BrowserName } from './registry';
|
||||||
import { registry } from './registry';
|
import { registry } from './registry';
|
||||||
import type { ConnectionTransport } from './transport';
|
import type { ConnectionTransport } from './transport';
|
||||||
@ -77,10 +77,17 @@ export abstract class BrowserType extends SdkObject {
|
|||||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
||||||
options = this._validateLaunchOptions(options);
|
options = this._validateLaunchOptions(options);
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const persistent: channels.BrowserNewContextParams = options;
|
const persistent: channels.BrowserNewContextParams = { ...options };
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
const browser = await controller.run(progress => {
|
const browser = await controller.run(async progress => {
|
||||||
return this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
|
// Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors.
|
||||||
|
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistent);
|
||||||
|
if (clientCertificatesProxy)
|
||||||
|
options.proxy = persistent.proxy;
|
||||||
|
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
|
||||||
|
const browser = await this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
|
||||||
|
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
|
||||||
|
return browser;
|
||||||
}, TimeoutSettings.launchTimeout(options));
|
}, TimeoutSettings.launchTimeout(options));
|
||||||
return browser._defaultContext!;
|
return browser._defaultContext!;
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
|
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import type { LookupAddress } from 'dns';
|
import type { LookupAddress } from 'dns';
|
||||||
import * as http from 'http';
|
import http from 'http';
|
||||||
import * as https from 'https';
|
import fs from 'fs';
|
||||||
|
import https from 'https';
|
||||||
import type { Readable, TransformCallback } from 'stream';
|
import type { Readable, TransformCallback } from 'stream';
|
||||||
import { pipeline, Transform } from 'stream';
|
import { pipeline, Transform } from 'stream';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
@ -27,7 +28,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';
|
||||||
@ -40,6 +41,7 @@ import { Tracing } from './trace/recorder/tracing';
|
|||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type { HeadersArray, ProxySettings } from './types';
|
import type { HeadersArray, ProxySettings } from './types';
|
||||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||||
|
import { clientCertificatesToTLSOptions } from './socksClientCertificatesInterceptor';
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
@ -49,6 +51,7 @@ type FetchRequestOptions = {
|
|||||||
timeoutSettings: TimeoutSettings;
|
timeoutSettings: TimeoutSettings;
|
||||||
ignoreHTTPSErrors?: boolean;
|
ignoreHTTPSErrors?: boolean;
|
||||||
baseURL?: string;
|
baseURL?: string;
|
||||||
|
clientCertificates?: channels.BrowserNewContextOptions['clientCertificates'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type HeadersObject = Readonly<{ [name: string]: string }>;
|
type HeadersObject = Readonly<{ [name: string]: string }>;
|
||||||
@ -190,9 +193,12 @@ export abstract class APIRequestContext extends SdkObject {
|
|||||||
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
|
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
|
||||||
timeout,
|
timeout,
|
||||||
deadline,
|
deadline,
|
||||||
|
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.toString()),
|
||||||
__testHookLookup: (params as any).__testHookLookup,
|
__testHookLookup: (params as any).__testHookLookup,
|
||||||
};
|
};
|
||||||
// rejectUnauthorized = undefined is treated as true in node 12.
|
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)
|
||||||
|
options.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
||||||
|
// rejectUnauthorized = undefined is treated as true in Node.js 12.
|
||||||
if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors)
|
if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors)
|
||||||
options.rejectUnauthorized = false;
|
options.rejectUnauthorized = false;
|
||||||
|
|
||||||
@ -351,6 +357,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||||||
maxRedirects: options.maxRedirects - 1,
|
maxRedirects: options.maxRedirects - 1,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
deadline: options.deadline,
|
deadline: options.deadline,
|
||||||
|
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.toString()),
|
||||||
__testHookLookup: options.__testHookLookup,
|
__testHookLookup: options.__testHookLookup,
|
||||||
};
|
};
|
||||||
// rejectUnauthorized = undefined is treated as true in node 12.
|
// rejectUnauthorized = undefined is treated as true in node 12.
|
||||||
@ -522,6 +529,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
|
|||||||
timeoutSettings: this._context._timeoutSettings,
|
timeoutSettings: this._context._timeoutSettings,
|
||||||
ignoreHTTPSErrors: this._context._options.ignoreHTTPSErrors,
|
ignoreHTTPSErrors: this._context._options.ignoreHTTPSErrors,
|
||||||
baseURL: this._context._options.baseURL,
|
baseURL: this._context._options.baseURL,
|
||||||
|
clientCertificates: this._context._options.clientCertificates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -557,17 +565,21 @@ 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(),
|
||||||
extraHTTPHeaders: options.extraHTTPHeaders,
|
extraHTTPHeaders: options.extraHTTPHeaders,
|
||||||
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
|
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
|
||||||
httpCredentials: options.httpCredentials,
|
httpCredentials: options.httpCredentials,
|
||||||
|
clientCertificates: options.clientCertificates,
|
||||||
proxy,
|
proxy,
|
||||||
timeoutSettings,
|
timeoutSettings,
|
||||||
};
|
};
|
||||||
|
@ -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 { globToRegex } 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 {
|
||||||
|
private readonly socksProxy: ClientCertificatesProxy;
|
||||||
|
private readonly uid: string;
|
||||||
|
private readonly host: string;
|
||||||
|
private readonly port: number;
|
||||||
|
firstPackageReceived: boolean = false;
|
||||||
|
target!: net.Socket;
|
||||||
|
// 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(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
|
||||||
|
this.socksProxy = socksProxy;
|
||||||
|
this.uid = uid;
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this.target = await createSocket(this.host === 'local.playwright' ? '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
|
||||||
|
if (data[0] === 0x16)
|
||||||
|
this._attachTLSListeners();
|
||||||
|
else
|
||||||
|
this.target.on('data', data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data }));
|
||||||
|
}
|
||||||
|
if (this.internal)
|
||||||
|
this.internal.push(data);
|
||||||
|
else
|
||||||
|
this.target.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _attachTLSListeners() {
|
||||||
|
this.internal = new SocksConnectionDuplex(data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data }));
|
||||||
|
const internalTLS = new tls.TLSSocket(this.internal, {
|
||||||
|
isServer: true,
|
||||||
|
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
||||||
|
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
||||||
|
});
|
||||||
|
this.internalTLS = internalTLS;
|
||||||
|
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}/`),
|
||||||
|
});
|
||||||
|
|
||||||
|
internalTLS.pipe(targetTLS);
|
||||||
|
targetTLS.pipe(internalTLS);
|
||||||
|
|
||||||
|
// Handle close and errors
|
||||||
|
const closeBothSockets = () => {
|
||||||
|
internalTLS.end();
|
||||||
|
targetTLS.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
internalTLS.on('end', () => closeBothSockets());
|
||||||
|
targetTLS.on('end', () => closeBothSockets());
|
||||||
|
|
||||||
|
internalTLS.on('error', () => closeBothSockets());
|
||||||
|
targetTLS.on('error', error => {
|
||||||
|
internalTLS.write('HTTP/1.1 503 Internal Server Error\r\n');
|
||||||
|
internalTLS.write('Content-Type: text/html; charset=utf-8\r\n');
|
||||||
|
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
||||||
|
internalTLS.write('Content-Length: ' + Buffer.byteLength(responseBody) + '\r\n');
|
||||||
|
internalTLS.write('\r\n');
|
||||||
|
internalTLS.write(responseBody);
|
||||||
|
internalTLS.end();
|
||||||
|
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> {
|
||||||
|
const port = await this._socksProxy.listen(0, '127.0.0.1');
|
||||||
|
return `socks5://127.0.0.1:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close() {
|
||||||
|
await this._socksProxy.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kClientCertificatesGlobRegex = Symbol('kClientCertificatesGlobRegex');
|
||||||
|
|
||||||
|
export function clientCertificatesToTLSOptions(
|
||||||
|
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
|
||||||
|
requestURL: string
|
||||||
|
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||||
|
const matchingCerts = clientCertificates?.filter(c => {
|
||||||
|
let regex: RegExp | undefined = (c as any)[kClientCertificatesGlobRegex];
|
||||||
|
if (!regex) {
|
||||||
|
regex = globToRegex(c.url);
|
||||||
|
(c as any)[kClientCertificatesGlobRegex] = regex;
|
||||||
|
}
|
||||||
|
regex.lastIndex = 0;
|
||||||
|
return regex.test(requestURL);
|
||||||
|
});
|
||||||
|
if (!matchingCerts || !matchingCerts.length)
|
||||||
|
return;
|
||||||
|
const tlsOptions = {
|
||||||
|
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)
|
||||||
|
tlsOptions.cert.push(cert.cert);
|
||||||
|
if (cert.key)
|
||||||
|
tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
|
||||||
|
if (cert.pfx)
|
||||||
|
tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tlsOptions;
|
||||||
|
}
|
172
packages/playwright-core/types/types.d.ts
vendored
172
packages/playwright-core/types/types.d.ts
vendored
@ -13221,6 +13221,49 @@ export interface BrowserType<Unused = {}> {
|
|||||||
*/
|
*/
|
||||||
chromiumSandbox?: boolean;
|
chromiumSandbox?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||||
|
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||||
|
* private key is encrypted. 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.
|
||||||
|
*
|
||||||
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
|
*
|
||||||
|
* **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
|
||||||
|
* work by replacing `localhost` with `local.playwright`.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the file with the private key in PEM format.
|
||||||
|
*/
|
||||||
|
keyPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
|
*/
|
||||||
|
pfxPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passphrase for the private key (PEM or PFX).
|
||||||
|
*/
|
||||||
|
passphrase?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
|
* 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.
|
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||||
@ -15590,6 +15633,49 @@ export interface APIRequest {
|
|||||||
*/
|
*/
|
||||||
baseURL?: string;
|
baseURL?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||||
|
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||||
|
* private key is encrypted. 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.
|
||||||
|
*
|
||||||
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
|
*
|
||||||
|
* **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
|
||||||
|
* work by replacing `localhost` with `local.playwright`.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the file with the private key in PEM format.
|
||||||
|
*/
|
||||||
|
keyPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
|
*/
|
||||||
|
pfxPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passphrase for the private key (PEM or PFX).
|
||||||
|
*/
|
||||||
|
passphrase?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
|
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
|
||||||
*/
|
*/
|
||||||
@ -16741,6 +16827,49 @@ export interface Browser extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
bypassCSP?: boolean;
|
bypassCSP?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||||
|
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||||
|
* private key is encrypted. 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.
|
||||||
|
*
|
||||||
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
|
*
|
||||||
|
* **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
|
||||||
|
* work by replacing `localhost` with `local.playwright`.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the file with the private key in PEM format.
|
||||||
|
*/
|
||||||
|
keyPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
|
*/
|
||||||
|
pfxPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passphrase for the private key (PEM or PFX).
|
||||||
|
*/
|
||||||
|
passphrase?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
|
* 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.
|
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||||
@ -20173,6 +20302,49 @@ export interface BrowserContextOptions {
|
|||||||
*/
|
*/
|
||||||
bypassCSP?: boolean;
|
bypassCSP?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||||
|
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||||
|
* private key is encrypted. 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.
|
||||||
|
*
|
||||||
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
|
*
|
||||||
|
* **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
|
||||||
|
* work by replacing `localhost` with `local.playwright`.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the file with the private key in PEM format.
|
||||||
|
*/
|
||||||
|
keyPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
|
*/
|
||||||
|
pfxPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passphrase for the private key (PEM or PFX).
|
||||||
|
*/
|
||||||
|
passphrase?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
|
* 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.
|
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||||
|
@ -140,6 +140,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||||||
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
|
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
|
||||||
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
|
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
|
||||||
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
|
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
|
||||||
|
clientCertificates: [({ contextOptions }, use) => use(contextOptions.clientCertificates), { option: true }],
|
||||||
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }],
|
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }],
|
||||||
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }],
|
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }],
|
||||||
viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }],
|
viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }],
|
||||||
@ -155,6 +156,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||||||
_combinedContextOptions: [async ({
|
_combinedContextOptions: [async ({
|
||||||
acceptDownloads,
|
acceptDownloads,
|
||||||
bypassCSP,
|
bypassCSP,
|
||||||
|
clientCertificates,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
deviceScaleFactor,
|
deviceScaleFactor,
|
||||||
extraHTTPHeaders,
|
extraHTTPHeaders,
|
||||||
@ -209,6 +211,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||||||
options.proxy = proxy;
|
options.proxy = proxy;
|
||||||
if (storageState !== undefined)
|
if (storageState !== undefined)
|
||||||
options.storageState = storageState;
|
options.storageState = storageState;
|
||||||
|
if (clientCertificates?.length)
|
||||||
|
options.clientCertificates = resolveClientCerticates(clientCertificates);
|
||||||
if (timezoneId !== undefined)
|
if (timezoneId !== undefined)
|
||||||
options.timezoneId = timezoneId;
|
options.timezoneId = timezoneId;
|
||||||
if (userAgent !== undefined)
|
if (userAgent !== undefined)
|
||||||
@ -416,6 +420,28 @@ function attachConnectedHeaderIfNeeded(testInfo: TestInfo, browser: Browser | nu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveFileToConfig(file: string | undefined) {
|
||||||
|
const config = test.info().config.configFile;
|
||||||
|
if (!config || !file)
|
||||||
|
return file;
|
||||||
|
if (path.isAbsolute(file))
|
||||||
|
return file;
|
||||||
|
return path.resolve(path.dirname(config), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientCertificates = NonNullable<PlaywrightTestOptions['clientCertificates']>;
|
||||||
|
|
||||||
|
function resolveClientCerticates(clientCertificates: ClientCertificates): ClientCertificates {
|
||||||
|
for (const { certs } of clientCertificates) {
|
||||||
|
for (const cert of certs) {
|
||||||
|
cert.certPath = resolveFileToConfig(cert.certPath);
|
||||||
|
cert.keyPath = resolveFileToConfig(cert.keyPath);
|
||||||
|
cert.pfxPath = resolveFileToConfig(cert.pfxPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clientCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
const kTracingStarted = Symbol('kTracingStarted');
|
const kTracingStarted = Symbol('kTracingStarted');
|
||||||
const kIsReusedContext = Symbol('kReusedContext');
|
const kIsReusedContext = Symbol('kReusedContext');
|
||||||
|
|
||||||
|
40
packages/playwright/types/test.d.ts
vendored
40
packages/playwright/types/test.d.ts
vendored
@ -4823,6 +4823,7 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
|
|||||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
|
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
|
||||||
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
|
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
|
||||||
|
type ClientCertificate = Exclude<BrowserContextOptions['clientCertificates'], undefined>[0];
|
||||||
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
|
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
|
||||||
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
|
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
|
||||||
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
|
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
|
||||||
@ -5200,6 +5201,45 @@ export interface PlaywrightTestOptions {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme;
|
||||||
|
/**
|
||||||
|
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||||
|
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||||
|
* private key is encrypted. 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.
|
||||||
|
*
|
||||||
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
|
*
|
||||||
|
* **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
|
||||||
|
* work by replacing `localhost` with `local.playwright`.
|
||||||
|
*
|
||||||
|
* **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: [{
|
||||||
|
* certPath: './cert.pem',
|
||||||
|
* keyPath: './key.pem',
|
||||||
|
* passphase: 'mysecretpassword',
|
||||||
|
* }],
|
||||||
|
* }],
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* ]
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
clientCertificates: ClientCertificate[] | undefined;
|
||||||
/**
|
/**
|
||||||
* Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about
|
* 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).
|
* [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices).
|
||||||
|
@ -576,6 +576,15 @@ export type PlaywrightNewRequestParams = {
|
|||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
@ -600,6 +609,15 @@ export type PlaywrightNewRequestOptions = {
|
|||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
extraHTTPHeaders?: NameValue[],
|
extraHTTPHeaders?: NameValue[],
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
httpCredentials?: {
|
httpCredentials?: {
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
@ -944,6 +962,15 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -1017,6 +1044,15 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -1131,6 +1167,15 @@ export type BrowserNewContextParams = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -1190,6 +1235,15 @@ export type BrowserNewContextOptions = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -1252,6 +1306,15 @@ export type BrowserNewContextForReuseParams = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -1311,6 +1374,15 @@ export type BrowserNewContextForReuseOptions = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -4558,6 +4630,15 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
@ -4615,6 +4696,15 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
clientCertificates?: {
|
||||||
|
url: string,
|
||||||
|
certs: {
|
||||||
|
cert?: Binary,
|
||||||
|
key?: Binary,
|
||||||
|
passphrase?: string,
|
||||||
|
pfx?: Binary,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
|
@ -433,6 +433,21 @@ ContextOptions:
|
|||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
ignoreHTTPSErrors: boolean?
|
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?
|
javaScriptEnabled: boolean?
|
||||||
bypassCSP: boolean?
|
bypassCSP: boolean?
|
||||||
userAgent: string?
|
userAgent: string?
|
||||||
@ -673,6 +688,21 @@ Playwright:
|
|||||||
extraHTTPHeaders:
|
extraHTTPHeaders:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
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:
|
httpCredentials:
|
||||||
type: object?
|
type: object?
|
||||||
properties:
|
properties:
|
||||||
|
59
tests/assets/client-certificates/README.md
Normal file
59
tests/assets/client-certificates/README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Client Certificate test-certificates
|
||||||
|
|
||||||
|
## Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req \
|
||||||
|
-x509 \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-keyout server/server_key.pem \
|
||||||
|
-out server/server_cert.pem \
|
||||||
|
-nodes \
|
||||||
|
-days 365 \
|
||||||
|
-subj "/CN=localhost/O=Client\ Certificate\ Demo"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trusted client-certificate (server signed/valid)
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p client/trusted
|
||||||
|
# generate server-signed (valid) certifcate
|
||||||
|
openssl req \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-keyout client/trusted/key.pem \
|
||||||
|
-out client/trusted/csr.pem \
|
||||||
|
-nodes \
|
||||||
|
-days 365 \
|
||||||
|
-subj "/CN=Alice"
|
||||||
|
|
||||||
|
# sign with server_cert.pem
|
||||||
|
openssl x509 \
|
||||||
|
-req \
|
||||||
|
-in client/trusted/csr.pem \
|
||||||
|
-CA server/server_cert.pem \
|
||||||
|
-CAkey server/server_key.pem \
|
||||||
|
-out client/trusted/cert.pem \
|
||||||
|
-set_serial 01 \
|
||||||
|
-days 365
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-signed certificate (invalid)
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p client/self-signed
|
||||||
|
openssl req \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-keyout client/self-signed/key.pem \
|
||||||
|
-out client/self-signed/csr.pem \
|
||||||
|
-nodes \
|
||||||
|
-days 365 \
|
||||||
|
-subj "/CN=Bob"
|
||||||
|
|
||||||
|
# sign with self-signed/key.pem
|
||||||
|
openssl x509 \
|
||||||
|
-req \
|
||||||
|
-in client/self-signed/csr.pem \
|
||||||
|
-signkey client/self-signed/key.pem \
|
||||||
|
-out client/self-signed/cert.pem \
|
||||||
|
-days 365
|
||||||
|
```
|
28
tests/assets/client-certificates/client/self-signed/cert.pem
Normal file
28
tests/assets/client-certificates/client/self-signed/cert.pem
Normal 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-----
|
26
tests/assets/client-certificates/client/self-signed/csr.pem
Normal file
26
tests/assets/client-certificates/client/self-signed/csr.pem
Normal 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-----
|
52
tests/assets/client-certificates/client/self-signed/key.pem
Normal file
52
tests/assets/client-certificates/client/self-signed/key.pem
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
n8nUUVeSi2z5bdctVUaQjQqc6zKI
|
||||||
|
-----END PRIVATE KEY-----
|
29
tests/assets/client-certificates/client/trusted/cert.pem
Normal file
29
tests/assets/client-certificates/client/trusted/cert.pem
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
26
tests/assets/client-certificates/client/trusted/csr.pem
Normal file
26
tests/assets/client-certificates/client/trusted/csr.pem
Normal 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-----
|
52
tests/assets/client-certificates/client/trusted/key.pem
Normal file
52
tests/assets/client-certificates/client/trusted/key.pem
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
XJVwXoZ9PUMxrfgEI9YmZdHOSJdU9Qc=
|
||||||
|
-----END PRIVATE KEY-----
|
31
tests/assets/client-certificates/server/server_cert.pem
Normal file
31
tests/assets/client-certificates/server/server_cert.pem
Normal 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-----
|
52
tests/assets/client-certificates/server/server_key.pem
Normal file
52
tests/assets/client-certificates/server/server_key.pem
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
m3Ea0Y3lf0rJCBJ10pFBP3z8orJyWkI=
|
||||||
|
-----END PRIVATE KEY-----
|
264
tests/library/client-certificates.spec.ts
Normal file
264
tests/library/client-certificates.spec.ts
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
/**
|
||||||
|
* 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 { expect, playwrightTest as base } from '../config/browserTest';
|
||||||
|
import type net from 'net';
|
||||||
|
import type { BrowserContextOptions } from 'packages/playwright-test';
|
||||||
|
const { createHttpsServer } = require('../../packages/playwright-core/lib/utils');
|
||||||
|
|
||||||
|
const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: string }>({
|
||||||
|
serverURL: async ({ asset }, use) => {
|
||||||
|
const server = createHttpsServer({
|
||||||
|
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
|
||||||
|
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||||
|
ca: [
|
||||||
|
fs.readFileSync(asset('client-certificates/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.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem');
|
||||||
|
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 = 'local.playwright';
|
||||||
|
const shouldRewriteToLocalhost = browserName === 'webkit' && process.platform === 'darwin';
|
||||||
|
await use(shouldRewriteToLocalhost ? parsed.toString() : serverURL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
|
|
||||||
|
const kDummyFileName = __filename;
|
||||||
|
const kValidationSubTests: [BrowserContextOptions, string][] = [
|
||||||
|
[{ 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: [{
|
||||||
|
certPath: kDummyFileName,
|
||||||
|
keyPath: kDummyFileName,
|
||||||
|
pfxPath: kDummyFileName,
|
||||||
|
passphrase: kDummyFileName,
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}, 'pfx is specified together with cert, key or passphrase'],
|
||||||
|
[{
|
||||||
|
proxy: { server: 'http://localhost:8080' },
|
||||||
|
clientCertificates: [{
|
||||||
|
url: 'test',
|
||||||
|
certs: [{
|
||||||
|
certPath: kDummyFileName,
|
||||||
|
keyPath: kDummyFileName,
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}, 'Cannot specify both proxy and clientCertificates'],
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe('fetch', () => {
|
||||||
|
test('validate input', async ({ playwright }) => {
|
||||||
|
for (const [contextOptions, expected] of kValidationSubTests)
|
||||||
|
await expect(playwright.request.newContext(contextOptions)).rejects.toThrow(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail with no client certificates provided', async ({ playwright, serverURL }) => {
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
|
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, asset }) => {
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: server.PREFIX,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/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 untrusted client certs', async ({ playwright, serverURL, asset }) => {
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: serverURL,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/self-signed/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('pass with trusted client certificates', async ({ playwright, serverURL, asset }) => {
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: serverURL,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
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, asset }) => {
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: serverURL,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/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 [contextOptions, expected] of kValidationSubTests)
|
||||||
|
await expect(browser.newContext(contextOptions)).rejects.toThrow(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should keep supporting http', async ({ browser, server, asset }) => {
|
||||||
|
const page = await browser.newPage({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: server.PREFIX,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/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, asset }) => {
|
||||||
|
const page = await browser.newPage({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: 'https://not-matching.com',
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/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 self-signed client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
||||||
|
const page = await browser.newPage({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: serverURLRewrittenToLocalhost,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/self-signed/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, asset }) => {
|
||||||
|
const page = await browser.newPage({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: serverURLRewrittenToLocalhost,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await page.goto(serverURLRewrittenToLocalhost);
|
||||||
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('persistentContext', () => {
|
||||||
|
test('validate input', async ({ launchPersistent }) => {
|
||||||
|
test.slow();
|
||||||
|
for (const [contextOptions, expected] of kValidationSubTests)
|
||||||
|
await expect(launchPersistent(contextOptions)).rejects.toThrow(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass with matching certificates', async ({ launchPersistent, serverURLRewrittenToLocalhost, asset }) => {
|
||||||
|
const { page } = await launchPersistent({
|
||||||
|
clientCertificates: [{
|
||||||
|
url: serverURLRewrittenToLocalhost,
|
||||||
|
certs: [{
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await page.goto(serverURLRewrittenToLocalhost);
|
||||||
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
@ -152,6 +152,7 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
|
|||||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
|
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
|
||||||
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
|
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
|
||||||
|
type ClientCertificate = Exclude<BrowserContextOptions['clientCertificates'], undefined>[0];
|
||||||
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
|
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
|
||||||
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
|
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
|
||||||
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
|
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
|
||||||
@ -209,6 +210,7 @@ export interface PlaywrightTestOptions {
|
|||||||
acceptDownloads: boolean;
|
acceptDownloads: boolean;
|
||||||
bypassCSP: boolean;
|
bypassCSP: boolean;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme;
|
||||||
|
clientCertificates: ClientCertificate[] | undefined;
|
||||||
deviceScaleFactor: number | undefined;
|
deviceScaleFactor: number | undefined;
|
||||||
extraHTTPHeaders: ExtraHTTPHeaders | undefined;
|
extraHTTPHeaders: ExtraHTTPHeaders | undefined;
|
||||||
geolocation: Geolocation | undefined;
|
geolocation: Geolocation | undefined;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user