diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 49e0dc0dcc..a6f6628569 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -16,6 +16,8 @@ import fs from 'fs'; import tls from 'tls'; +import type https from 'https'; +import zlib from 'zlib'; import type http2 from 'http2'; import type http from 'http'; import { expect, playwrightTest as base } from '../config/browserTest'; @@ -375,6 +377,138 @@ test.describe('browser', () => { await page.close(); }); + test('should handle TLS renegotiation with client certificates', async ({ browser, asset, browserName, platform }) => { + const server: https.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: false, // Initially don't request client cert + rejectUnauthorized: false, + // TLSv1.3 does not support renegotiation + minVersion: 'TLSv1.2', + maxVersion: 'TLSv1.2', + }); + + server.on('request', async (req, res) => { + if (!req.socket) + return; + const renegotiate = () => new Promise((resolve, reject) => { + (req.socket as tls.TLSSocket).renegotiate({ + requestCert: true, + rejectUnauthorized: false + }, err => { + if (err) + reject(err); + else + resolve(); + }); + }); + if (req.url === '/') { + res.writeHead(200, { 'Content-Type': 'text/html', 'connection': 'close' }); + res.end(); + } else if (req.url === '/from-fetch-api') { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Transfer-Encoding': 'chunked' + }); + res.flushHeaders(); + + await new Promise(resolve => req.once('data', data => { + res.write(`server received: ${data.toString()}\n`); + resolve(); + })); + + await renegotiate(); + for (let i = 0; i < 4; i++) { + res.write(`${i}-from-server\n`); + // Best-effort to trigger a new chunk + await new Promise(resolve => setTimeout(resolve, 100)); + } + res.end('server closed the connection'); + } else if (req.url === '/style.css') { + res.writeHead(200, { + 'Content-Type': 'text/css', + 'Content-Encoding': 'gzip', + 'Transfer-Encoding': 'chunked' + }); + + await renegotiate(); + + const stylesheet = ` + button { + background-color: red; + } + `; + + const stylesheetBuffer = await new Promise((resolve, reject) => { + zlib.gzip(stylesheet, (err, buffer) => { + if (err) + reject(err); + else + resolve(buffer); + }); + }); + for (let i = 0; i < stylesheetBuffer.length; i += 100) { + res.write(stylesheetBuffer.slice(i, i + 100)); + // Best-effort to trigger a new chunk + await new Promise(resolve => setTimeout(resolve, 20)); + } + res.end(); + } else { + res.writeHead(404); + res.end(); + } + }); + + await new Promise(resolve => server.listen(0, 'localhost', resolve)); + const port = (server.address() as import('net').AddressInfo).port; + const origin = 'https://' + (browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost'); + const serverUrl = `${origin}:${port}`; + + const context = await browser.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }); + + const page = await context.newPage(); + + await test.step('fetch API', async () => { + await page.goto(serverUrl); + const response = await page.evaluate(async () => { + const response = await fetch('/from-fetch-api', { + method: 'POST', + body: 'client-request-payload' + }); + return await response.text(); + }); + expect(response).toBe([ + 'server received: client-request-payload', + '0-from-server', + '1-from-server', + '2-from-server', + '3-from-server', + 'server closed the connection' + ].join('\n')); + }); + + await test.step('Gzip encoded CSS Stylesheet', async () => { + await page.goto(serverUrl); + // The would throw with net::ERR_INVALID_CHUNKED_ENCODING + await page.setContent(` + + + `); + await expect(page.locator('button')).toHaveCSS('background-color', /* red */'rgb(255, 0, 0)'); + }); + + await context.close(); + await new Promise(resolve => server.close(() => resolve())); + }); + test('should pass with matching certificates in pfx format when passing as content', async ({ browser, startCCServer, asset, browserName }) => { const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); const page = await browser.newPage({