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

Extracted from https://github.com/microsoft/playwright/pull/32158.
This commit is contained in:
Max Schmitt 2024-08-15 08:51:40 +02:00 committed by GitHub
parent f927495791
commit aac3a84321
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 10 deletions

View File

@ -60,11 +60,9 @@ class ALPNCache {
ALPNProtocols: ['h2', 'http/1.1'],
rejectUnauthorized: false,
}).then(socket => {
socket.once('secureConnect', () => {
// The server may not respond with ALPN, in which case we default to http/1.1.
result.resolve(socket.alpnProtocol || 'http/1.1');
socket.end();
});
// The server may not respond with ALPN, in which case we default to http/1.1.
result.resolve(socket.alpnProtocol || 'http/1.1');
socket.end();
}).catch(error => {
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
result.resolve('http/1.1');
@ -213,8 +211,7 @@ class SocksProxyConnection {
targetTLS.pipe(internalTLS);
});
internalTLS.once('end', () => closeBothSockets());
targetTLS.once('end', () => closeBothSockets());
internalTLS.once('close', () => closeBothSockets());
internalTLS.once('error', () => closeBothSockets());
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');
if (net.isIP(options.host)) {
const socket = tls.connect(options)
socket.on('connect', () => resolve(socket));
socket.on('secureConnect', () => resolve(socket));
socket.on('error', error => reject(error));
} else {
createConnectionAsync(options, (err, socket) => {
if (err)
reject(err);
if (socket)
resolve(socket);
if (socket) {
socket.on('secureConnect', () => resolve(socket));
socket.on('error', error => reject(error));
}
}, true).catch(err => reject(err));
}
});

View File

@ -15,6 +15,7 @@
*/
import fs from 'fs';
import tls from 'tls';
import type http2 from 'http2';
import type http from 'http';
import { expect, playwrightTest as base } from '../config/browserTest';
@ -302,6 +303,48 @@ test.describe('browser', () => {
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 }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
const page = await browser.newPage({