2024-07-12 11:42:24 +02:00
|
|
|
/**
|
|
|
|
* 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';
|
2024-07-23 19:18:31 +02:00
|
|
|
import http2 from 'http2';
|
|
|
|
import type http from 'http';
|
2024-07-12 11:42:24 +02:00
|
|
|
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');
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
type TestOptions = {
|
|
|
|
startCCServer(options?: {
|
|
|
|
http2?: boolean;
|
|
|
|
useFakeLocalhost?: boolean;
|
|
|
|
}): Promise<string>,
|
|
|
|
};
|
|
|
|
|
|
|
|
const test = base.extend<TestOptions>({
|
|
|
|
startCCServer: async ({ asset, browserName }, use) => {
|
2024-07-12 11:42:24 +02:00
|
|
|
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem');
|
2024-07-23 19:18:31 +02:00
|
|
|
let server: http.Server | http2.Http2Server | undefined;
|
|
|
|
await use(async options => {
|
|
|
|
server = (options?.http2 ? http2.createSecureServer : 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,
|
2024-07-24 11:39:39 +02:00
|
|
|
allowHTTP1: true,
|
2024-07-23 19:18:31 +02:00
|
|
|
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
|
|
|
|
const tlsSocket = req.socket as import('tls').TLSSocket;
|
|
|
|
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62336
|
|
|
|
expect(['localhost', 'local.playwright'].includes((tlsSocket).servername)).toBe(true);
|
2024-07-24 11:39:39 +02:00
|
|
|
const prefix = `ALPN protocol: ${tlsSocket.alpnProtocol}\n`;
|
2024-07-23 19:18:31 +02:00
|
|
|
const cert = tlsSocket.getPeerCertificate();
|
|
|
|
if (tlsSocket.authorized) {
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
2024-07-24 11:39:39 +02:00
|
|
|
res.end(prefix + `Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
|
2024-07-23 19:18:31 +02:00
|
|
|
} else if (cert.subject) {
|
|
|
|
res.writeHead(403, { 'Content-Type': 'text/html' });
|
2024-07-24 11:39:39 +02:00
|
|
|
res.end(prefix + `Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
|
2024-07-23 19:18:31 +02:00
|
|
|
} else {
|
|
|
|
res.writeHead(401, { 'Content-Type': 'text/html' });
|
2024-07-24 11:39:39 +02:00
|
|
|
res.end(prefix + `Sorry, but you need to provide a client certificate to continue.`);
|
2024-07-23 19:18:31 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
|
|
|
|
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
|
|
|
|
return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
|
|
|
|
});
|
2024-07-24 11:39:39 +02:00
|
|
|
if (server)
|
|
|
|
await new Promise<void>(resolve => server.close(() => resolve()));
|
2024-07-12 11:42:24 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-07-18 13:43:56 +02:00
|
|
|
test.use({
|
|
|
|
launchOptions: async ({ launchOptions }, use) => {
|
|
|
|
await use({
|
|
|
|
...launchOptions,
|
|
|
|
proxy: { server: 'per-context' }
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-07-12 11:42:24 +02:00
|
|
|
test.skip(({ mode }) => mode !== 'default');
|
|
|
|
|
|
|
|
const kDummyFileName = __filename;
|
|
|
|
const kValidationSubTests: [BrowserContextOptions, string][] = [
|
2024-07-23 22:56:36 +02:00
|
|
|
[{ clientCertificates: [{ origin: 'test' }] }, 'None of cert, key, passphrase or pfx is specified'],
|
2024-07-12 11:42:24 +02:00
|
|
|
[{
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: 'test',
|
|
|
|
certPath: kDummyFileName,
|
|
|
|
keyPath: kDummyFileName,
|
|
|
|
pfxPath: kDummyFileName,
|
|
|
|
passphrase: kDummyFileName,
|
2024-07-12 11:42:24 +02:00
|
|
|
}]
|
|
|
|
}, 'pfx is specified together with cert, key or passphrase'],
|
|
|
|
[{
|
|
|
|
proxy: { server: 'http://localhost:8080' },
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: 'test',
|
|
|
|
certPath: kDummyFileName,
|
|
|
|
keyPath: kDummyFileName,
|
2024-07-12 11:42:24 +02:00
|
|
|
}]
|
|
|
|
}, '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);
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should fail with no client certificates provided', async ({ playwright, startCCServer }) => {
|
|
|
|
const serverURL = await startCCServer();
|
2024-07-12 11:42:24 +02:00
|
|
|
const request = await playwright.request.newContext();
|
|
|
|
const response = await request.get(serverURL);
|
|
|
|
expect(response.status()).toBe(401);
|
2024-07-24 11:39:39 +02:00
|
|
|
expect(await response.text()).toContain('Sorry, but you need to provide a client certificate to continue.');
|
2024-07-12 11:42:24 +02:00
|
|
|
await request.dispose();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should keep supporting http', async ({ playwright, server, asset }) => {
|
|
|
|
const request = await playwright.request.newContext({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(server.PREFIX).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
|
|
|
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();
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should throw with untrusted client certs', async ({ playwright, startCCServer, asset }) => {
|
|
|
|
const serverURL = await startCCServer();
|
2024-07-12 11:42:24 +02:00
|
|
|
const request = await playwright.request.newContext({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
|
|
|
const response = await request.get(serverURL);
|
|
|
|
expect(response.url()).toBe(serverURL);
|
|
|
|
expect(response.status()).toBe(403);
|
2024-07-24 11:39:39 +02:00
|
|
|
expect(await response.text()).toContain('Sorry Bob, certificates from Bob are not welcome here.');
|
2024-07-12 11:42:24 +02:00
|
|
|
await request.dispose();
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('pass with trusted client certificates', async ({ playwright, startCCServer, asset }) => {
|
|
|
|
const serverURL = await startCCServer();
|
2024-07-12 11:42:24 +02:00
|
|
|
const request = await playwright.request.newContext({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
|
|
|
const response = await request.get(serverURL);
|
|
|
|
expect(response.url()).toBe(serverURL);
|
|
|
|
expect(response.status()).toBe(200);
|
2024-07-24 11:39:39 +02:00
|
|
|
expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!');
|
2024-07-12 11:42:24 +02:00
|
|
|
await request.dispose();
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should work in the browser with request interception', async ({ browser, playwright, startCCServer, asset }) => {
|
|
|
|
const serverURL = await startCCServer();
|
2024-07-12 11:42:24 +02:00
|
|
|
const request = await playwright.request.newContext({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
|
|
|
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: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(server.PREFIX).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
|
|
|
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();
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should fail with no client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
|
|
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
2024-07-12 11:42:24 +02:00
|
|
|
const page = await browser.newPage({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: 'https://not-matching.com',
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
2024-07-23 19:18:31 +02:00
|
|
|
await page.goto(serverURL);
|
2024-07-12 11:42:24 +02:00
|
|
|
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
|
|
|
await page.close();
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should fail with self-signed client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
|
|
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
2024-07-12 11:42:24 +02:00
|
|
|
const page = await browser.newPage({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
2024-07-23 19:18:31 +02:00
|
|
|
await page.goto(serverURL);
|
2024-07-12 11:42:24 +02:00
|
|
|
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
|
|
|
|
await page.close();
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should pass with matching certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
|
|
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
2024-07-12 11:42:24 +02:00
|
|
|
const page = await browser.newPage({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
2024-07-23 19:18:31 +02:00
|
|
|
await page.goto(serverURL);
|
2024-07-12 11:42:24 +02:00
|
|
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
|
|
|
await page.close();
|
|
|
|
});
|
|
|
|
|
2024-07-19 12:04:12 +02:00
|
|
|
test('should have ignoreHTTPSErrors=false by default', async ({ browser, httpsServer, asset, browserName, platform }) => {
|
2024-07-18 22:37:11 +02:00
|
|
|
const page = await browser.newPage({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com',
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-18 22:37:11 +02:00
|
|
|
}],
|
|
|
|
});
|
2024-07-19 12:04:12 +02:00
|
|
|
await page.goto(browserName === 'webkit' && platform === 'darwin' ? httpsServer.EMPTY_PAGE.replace('localhost', 'local.playwright') : httpsServer.EMPTY_PAGE);
|
2024-07-18 22:37:11 +02:00
|
|
|
await expect(page.getByText('Playwright client-certificate error')).toBeVisible();
|
|
|
|
await page.close();
|
|
|
|
});
|
|
|
|
|
2024-07-24 11:39:39 +02:00
|
|
|
test('support http2', async ({ browser, startCCServer, asset, browserName }) => {
|
|
|
|
test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost');
|
|
|
|
const serverURL = await startCCServer({ http2: true });
|
|
|
|
const page = await browser.newPage({
|
|
|
|
clientCertificates: [{
|
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
|
|
|
}],
|
|
|
|
});
|
|
|
|
// TODO: We should investigate why http2 is not supported in WebKit on Linux.
|
|
|
|
// https://bugs.webkit.org/show_bug.cgi?id=276990
|
|
|
|
const expectedProtocol = browserName === 'webkit' && process.platform === 'linux' ? 'http/1.1' : 'h2';
|
|
|
|
{
|
|
|
|
await page.goto(serverURL.replace('localhost', 'local.playwright'));
|
|
|
|
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
|
|
|
await expect(page.getByText(`ALPN protocol: ${expectedProtocol}`)).toBeVisible();
|
|
|
|
}
|
|
|
|
{
|
|
|
|
await page.goto(serverURL);
|
|
|
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
|
|
|
await expect(page.getByText(`ALPN protocol: ${expectedProtocol}`)).toBeVisible();
|
|
|
|
}
|
|
|
|
await page.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('support http2 if the browser only supports http1.1', async ({ browserType, browserName, startCCServer, asset }) => {
|
|
|
|
test.skip(browserName !== 'chromium');
|
|
|
|
const serverURL = await startCCServer({ http2: true });
|
|
|
|
const browser = await browserType.launch({ args: ['--disable-http2'] });
|
|
|
|
const page = await browser.newPage({
|
|
|
|
clientCertificates: [{
|
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
|
|
|
}],
|
|
|
|
});
|
|
|
|
{
|
|
|
|
await page.goto(serverURL.replace('localhost', 'local.playwright'));
|
|
|
|
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
|
|
|
await expect(page.getByText('ALPN protocol: http/1.1')).toBeVisible();
|
|
|
|
}
|
|
|
|
{
|
|
|
|
await page.goto(serverURL);
|
|
|
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
|
|
|
await expect(page.getByText('ALPN protocol: http/1.1')).toBeVisible();
|
|
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
});
|
|
|
|
|
2024-07-12 11:42:24 +02:00
|
|
|
test.describe('persistentContext', () => {
|
|
|
|
test('validate input', async ({ launchPersistent }) => {
|
|
|
|
test.slow();
|
|
|
|
for (const [contextOptions, expected] of kValidationSubTests)
|
|
|
|
await expect(launchPersistent(contextOptions)).rejects.toThrow(expected);
|
|
|
|
});
|
|
|
|
|
2024-07-23 19:18:31 +02:00
|
|
|
test('should pass with matching certificates', async ({ launchPersistent, startCCServer, asset, browserName }) => {
|
|
|
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
2024-07-12 11:42:24 +02:00
|
|
|
const { page } = await launchPersistent({
|
|
|
|
clientCertificates: [{
|
2024-07-23 22:56:36 +02:00
|
|
|
origin: new URL(serverURL).origin,
|
|
|
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
|
|
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
2024-07-12 11:42:24 +02:00
|
|
|
}],
|
|
|
|
});
|
2024-07-23 19:18:31 +02:00
|
|
|
await page.goto(serverURL);
|
2024-07-12 11:42:24 +02:00
|
|
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|