test: use managed http2 server for client-certificates (#31844)

This commit is contained in:
Max Schmitt 2024-07-25 18:53:38 +02:00 committed by GitHub
parent b06a95dd75
commit 90af289ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 38 additions and 26 deletions

View File

@ -17,6 +17,7 @@
import http from 'http';
import https from 'https';
import http2 from 'http2';
import type net from 'net';
import { getProxyForUrl } from '../utilsBundle';
import { HttpsProxyAgent } from '../utilsBundle';
@ -169,6 +170,14 @@ export function createHttpsServer(...args: any[]): https.Server {
return server;
}
export function createHttp2Server( onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer;
export function createHttp2Server(options: http2.SecureServerOptions, onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer;
export function createHttp2Server(...args: any[]): http2.Http2SecureServer {
const server = http2.createSecureServer(...args);
decorateServer(server);
return server;
}
export async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void) {
let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onLog, onStdErr);
if (statusCode === 404 && url.pathname === '/') {
@ -200,7 +209,7 @@ async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onLog?: (dat
});
}
function decorateServer(server: http.Server | http.Server) {
function decorateServer(server: net.Server) {
const sockets = new Set<net.Socket>();
server.on('connection', socket => {
sockets.add(socket);

View File

@ -15,12 +15,12 @@
*/
import fs from 'fs';
import http2 from 'http2';
import type http2 from 'http2';
import type http from 'http';
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 { createHttpsServer, createHttp2Server } = require('../../packages/playwright-core/lib/utils');
type TestOptions = {
startCCServer(options?: {
@ -30,11 +30,11 @@ type TestOptions = {
};
const test = base.extend<TestOptions>({
startCCServer: async ({ asset, browserName }, use) => {
startCCServer: async ({ asset }, use) => {
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem');
let server: http.Server | http2.Http2Server | undefined;
let server: http.Server | http2.Http2SecureServer | undefined;
await use(async options => {
server = (options?.http2 ? http2.createSecureServer : createHttpsServer)({
server = (options?.http2 ? createHttp2Server : createHttpsServer)({
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
ca: [
@ -45,20 +45,22 @@ const test = base.extend<TestOptions>({
allowHTTP1: true,
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
const tlsSocket = req.socket as import('tls').TLSSocket;
const parts: { key: string, value: any }[] = [];
parts.push({ key: 'alpn-protocol', value: tlsSocket.alpnProtocol });
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62336
expect(['localhost', 'local.playwright'].includes((tlsSocket).servername)).toBe(true);
const prefix = `ALPN protocol: ${tlsSocket.alpnProtocol}\n`;
parts.push({ key: 'servername', value: tlsSocket.servername });
const cert = tlsSocket.getPeerCertificate();
if (tlsSocket.authorized) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(prefix + `Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
parts.push({ key: 'message', value: `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(prefix + `Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
parts.push({ key: 'message', value: `Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.` });
} else {
res.writeHead(401, { 'Content-Type': 'text/html' });
res.end(prefix + `Sorry, but you need to provide a client certificate to continue.`);
parts.push({ key: 'message', value: `Sorry, but you need to provide a client certificate to continue.` });
}
res.end(parts.map(({ key, value }) => `<div data-testid="${key}">${value}</div>`).join(''));
});
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
@ -179,7 +181,7 @@ test.describe('fetch', () => {
await route.fulfill({ response });
});
await page.goto(serverURL);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close();
await request.dispose();
});
@ -216,7 +218,7 @@ test.describe('browser', () => {
}],
});
await page.goto(serverURL);
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
await page.close();
});
@ -230,7 +232,7 @@ test.describe('browser', () => {
}],
});
await page.goto(serverURL);
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
await expect(page.getByTestId('message')).toHaveText('Sorry Bob, certificates from Bob are not welcome here.');
await page.close();
});
@ -244,7 +246,7 @@ test.describe('browser', () => {
}],
});
await page.goto(serverURL);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close();
});
@ -290,13 +292,14 @@ test.describe('browser', () => {
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 expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
await expect(page.getByTestId('alpn-protocol')).toHaveText(expectedProtocol);
await expect(page.getByTestId('servername')).toHaveText('local.playwright');
}
{
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 expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await expect(page.getByTestId('alpn-protocol')).toHaveText(expectedProtocol);
}
await page.close();
});
@ -314,13 +317,13 @@ test.describe('browser', () => {
});
{
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 expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
await expect(page.getByTestId('alpn-protocol')).toHaveText('http/1.1');
}
{
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 expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await expect(page.getByTestId('alpn-protocol')).toHaveText('http/1.1');
}
await browser.close();
});
@ -342,7 +345,7 @@ test.describe('browser', () => {
}],
});
await page.goto(serverURL);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
});
});
});

View File

@ -18,11 +18,11 @@
import { browserTest as it, expect } from '../config/browserTest';
import * as path from 'path';
import fs from 'fs';
import http2 from 'http2';
import type { BrowserContext, BrowserContextOptions } from 'playwright-core';
import type { AddressInfo } from 'net';
import type { Log } from '../../packages/trace/src/har';
import { parseHar } from '../config/utils';
const { createHttp2Server } = require('../../packages/playwright-core/lib/utils');
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean } = {}) {
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
@ -686,7 +686,7 @@ it('should return security details directly from response', async ({ contextFact
});
it('should contain http2 for http2 requests', async ({ contextFactory }, testInfo) => {
const server = http2.createSecureServer({
const server = createHttp2Server({
key: await fs.promises.readFile(path.join(__dirname, '..', 'config', 'testserver', 'key.pem')),
cert: await fs.promises.readFile(path.join(__dirname, '..', 'config', 'testserver', 'cert.pem')),
});