From 90af289ba21dcfbd7b0e4e7f6718ebcd3ded83ea Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 25 Jul 2024 18:53:38 +0200 Subject: [PATCH] test: use managed http2 server for client-certificates (#31844) --- packages/playwright-core/src/utils/network.ts | 11 ++++- tests/library/client-certificates.spec.ts | 49 ++++++++++--------- tests/library/har.spec.ts | 4 +- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/packages/playwright-core/src/utils/network.ts b/packages/playwright-core/src/utils/network.ts index 8cacb8aeb6..b9bc2c0f06 100644 --- a/packages/playwright-core/src/utils/network.ts +++ b/packages/playwright-core/src/utils/network.ts @@ -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(); server.on('connection', socket => { sockets.add(socket); diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index c5fc523925..ff60010d5b 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -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({ - 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({ 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 }) => `
${value}
`).join('')); }); await new Promise(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!'); }); }); }); diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index 41723dd7af..29ee7df2d1 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -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, 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')), });