cherry-pick(#32163): fix(client-certificates): stall on tls handshake errors

Extracted from https://github.com/microsoft/playwright/pull/32158.
This commit is contained in:
Max Schmitt 2024-08-15 10:26:04 +02:00
parent bd13da4132
commit d78ae0179d
3 changed files with 52 additions and 10 deletions

View file

@ -60,11 +60,9 @@ class ALPNCache {
ALPNProtocols: ['h2', 'http/1.1'], ALPNProtocols: ['h2', 'http/1.1'],
rejectUnauthorized: false, rejectUnauthorized: false,
}).then(socket => { }).then(socket => {
socket.once('secureConnect', () => { // The server may not respond with ALPN, in which case we default to http/1.1.
// The server may not respond with ALPN, in which case we default to http/1.1. result.resolve(socket.alpnProtocol || 'http/1.1');
result.resolve(socket.alpnProtocol || 'http/1.1'); socket.end();
socket.end();
});
}).catch(error => { }).catch(error => {
debugLogger.log('client-certificates', `ALPN error: ${error.message}`); debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
result.resolve('http/1.1'); result.resolve('http/1.1');
@ -213,8 +211,7 @@ class SocksProxyConnection {
targetTLS.pipe(internalTLS); targetTLS.pipe(internalTLS);
}); });
internalTLS.once('end', () => closeBothSockets()); internalTLS.once('close', () => closeBothSockets());
targetTLS.once('end', () => closeBothSockets());
internalTLS.once('error', () => closeBothSockets()); internalTLS.once('error', () => closeBothSockets());
targetTLS.once('error', handleError); targetTLS.once('error', handleError);

View file

@ -72,14 +72,16 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise<t
assert(options.host, 'host is required'); assert(options.host, 'host is required');
if (net.isIP(options.host)) { if (net.isIP(options.host)) {
const socket = tls.connect(options) const socket = tls.connect(options)
socket.on('connect', () => resolve(socket)); socket.on('secureConnect', () => resolve(socket));
socket.on('error', error => reject(error)); socket.on('error', error => reject(error));
} else { } else {
createConnectionAsync(options, (err, socket) => { createConnectionAsync(options, (err, socket) => {
if (err) if (err)
reject(err); reject(err);
if (socket) if (socket) {
resolve(socket); socket.on('secureConnect', () => resolve(socket));
socket.on('error', error => reject(error));
}
}, true).catch(err => reject(err)); }, true).catch(err => reject(err));
} }
}); });

View file

@ -15,6 +15,7 @@
*/ */
import fs from 'fs'; import fs from 'fs';
import tls from 'tls';
import type http2 from 'http2'; import type http2 from 'http2';
import type http from 'http'; import type http from 'http';
import { expect, playwrightTest as base } from '../config/browserTest'; import { expect, playwrightTest as base } from '../config/browserTest';
@ -302,6 +303,48 @@ test.describe('browser', () => {
await page.close(); await page.close();
}); });
test('should not hang on tls errors during TLS 1.2 handshake', async ({ browser, asset, platform, browserName }) => {
for (const tlsVersion of ['TLSv1.3', 'TLSv1.2'] as const) {
await test.step(`TLS version: ${tlsVersion}`, async () => {
const server = tls.createServer({
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: true,
minVersion: tlsVersion,
maxVersion: tlsVersion,
SNICallback: (servername, cb) => {
// Always reject the connection by passing an error
cb(new Error('Connection rejected'), null);
}
}, () => {
// Do nothing
});
const serverURL = await new Promise<string>(resolve => {
server.listen(0, 'localhost', () => {
const host = browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost';
resolve(`https://${host}:${(server.address() as net.AddressInfo).port}/`);
});
});
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: new URL(serverURL).origin,
certPath: asset('client-certificates/client/self-signed/cert.pem'),
keyPath: asset('client-certificates/client/self-signed/key.pem'),
}],
});
await page.goto(serverURL);
await expect(page.getByText('Playwright client-certificate error: Client network socket disconnected before secure TLS connection was established')).toBeVisible();
await page.close();
await new Promise<void>(resolve => server.close(() => resolve()));
});
}
});
test('should pass with matching certificates in pfx format', async ({ browser, startCCServer, asset, browserName }) => { test('should pass with matching certificates in pfx format', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
const page = await browser.newPage({ const page = await browser.newPage({