fix(client-certificates): return target errors on response when using http2 (#31867)

This commit is contained in:
Max Schmitt 2024-07-26 11:28:45 +02:00 committed by GitHub
parent 335d31bf65
commit 09581b615d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 12 deletions

View File

@ -16,6 +16,7 @@
import net from 'net';
import path from 'path';
import http2 from 'http2';
import type https from 'https';
import fs from 'fs';
import tls from 'tls';
@ -80,17 +81,19 @@ class SocksProxyConnection {
target!: net.Socket;
// In case of http, we just pipe data to the target socket and they are |undefined|.
internal: stream.Duplex | undefined;
private _targetCloseEventListener: () => void;
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
this.socksProxy = socksProxy;
this.uid = uid;
this.host = host;
this.port = port;
this._targetCloseEventListener = () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid });
}
async connect() {
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
this.target.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
this.target.on('close', this._targetCloseEventListener);
this.target.on('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
this.socksProxy._socksProxy.socketConnected({
uid: this.uid,
@ -139,7 +142,6 @@ class SocksProxyConnection {
dummyServer.emit('connection', this.internal);
dummyServer.on('secureConnection', internalTLS => {
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`);
internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
const tlsOptions: tls.ConnectionOptions = {
socket: this.target,
host: this.host,
@ -154,8 +156,10 @@ class SocksProxyConnection {
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
const targetTLS = tls.connect(tlsOptions);
internalTLS.pipe(targetTLS);
targetTLS.pipe(internalTLS);
targetTLS.on('secureConnect', () => {
internalTLS.pipe(targetTLS);
targetTLS.pipe(internalTLS);
});
// Handle close and errors
const closeBothSockets = () => {
@ -169,11 +173,30 @@ class SocksProxyConnection {
internalTLS.on('error', () => closeBothSockets());
targetTLS.on('error', error => {
debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`);
const responseBody = 'Playwright client-certificate error: ' + error.message;
if (internalTLS?.alpnProtocol === 'h2') {
// https://github.com/nodejs/node/issues/46152
// TODO: http2.performServerHandshake does not work here for some reason.
// This method is available only in Node.js 20+
if ('performServerHandshake' in http2) {
// In case of an 'error' event on the target connection, we still need to perform the http2 handshake on the browser side.
// This is an async operation, so we need to intercept the close event to prevent the socket from being closed too early.
this.target.removeListener('close', this._targetCloseEventListener);
// @ts-expect-error
const session: http2.ServerHttp2Session = http2.performServerHandshake(internalTLS);
session.on('stream', (stream: http2.ServerHttp2Stream) => {
stream.respond({
'content-type': 'text/html',
[http2.constants.HTTP2_HEADER_STATUS]: 503,
});
stream.end(responseBody, () => {
session.close();
closeBothSockets();
});
stream.on('error', () => closeBothSockets());
});
} else {
closeBothSockets();
}
} else {
const responseBody = 'Playwright client-certificate error: ' + error.message;
internalTLS.end([
'HTTP/1.1 503 Internal Server Error',
'Content-Type: text/html; charset=utf-8',
@ -181,8 +204,8 @@ class SocksProxyConnection {
'\r\n',
responseBody,
].join('\r\n'));
closeBothSockets();
}
closeBothSockets();
});
});
});

View File

@ -25,6 +25,7 @@ const { createHttpsServer, createHttp2Server } = require('../../packages/playwri
type TestOptions = {
startCCServer(options?: {
http2?: boolean;
enableHTTP1FallbackWhenUsingHttp2?: boolean;
useFakeLocalhost?: boolean;
}): Promise<string>,
};
@ -42,7 +43,7 @@ const test = base.extend<TestOptions>({
],
requestCert: true,
rejectUnauthorized: false,
allowHTTP1: true,
allowHTTP1: options?.enableHTTP1FallbackWhenUsingHttp2,
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
const tlsSocket = req.socket as import('tls').TLSSocket;
const parts: { key: string, value: any }[] = [];
@ -279,7 +280,8 @@ test.describe('browser', () => {
test('support http2', async ({ browser, startCCServer, asset, browserName }) => {
test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost');
const serverURL = await startCCServer({ http2: true });
const enableHTTP1FallbackWhenUsingHttp2 = browserName === 'webkit' && process.platform === 'linux';
const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2 });
const page = await browser.newPage({
clientCertificates: [{
origin: new URL(serverURL).origin,
@ -289,7 +291,7 @@ test.describe('browser', () => {
});
// TODO: We should investigate why http2 is not supported in WebKit on Linux.
// https://bugs.webkit.org/show_bug.cgi?id=276990
const expectedProtocol = browserName === 'webkit' && process.platform === 'linux' ? 'http/1.1' : 'h2';
const expectedProtocol = enableHTTP1FallbackWhenUsingHttp2 ? 'http/1.1' : 'h2';
{
await page.goto(serverURL.replace('localhost', 'local.playwright'));
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
@ -306,7 +308,7 @@ test.describe('browser', () => {
test('support http2 if the browser only supports http1.1', async ({ browserType, browserName, startCCServer, asset }) => {
test.skip(browserName !== 'chromium');
const serverURL = await startCCServer({ http2: true });
const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2: true });
const browser = await browserType.launch({ args: ['--disable-http2'] });
const page = await browser.newPage({
clientCertificates: [{
@ -328,6 +330,25 @@ test.describe('browser', () => {
await browser.close();
});
test('should return target connection errors when using http2', async ({ browser, startCCServer, asset, browserName }) => {
test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost');
test.fixme(browserName === 'webkit' && process.platform === 'linux', 'WebKit on Linux does not support http2 https://bugs.webkit.org/show_bug.cgi?id=276990');
test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions');
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('empty.html');
const serverURL = await startCCServer({ http2: true });
const page = await browser.newPage({
clientCertificates: [{
origin: 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com',
certPath: asset('client-certificates/client/trusted/cert.pem'),
keyPath: asset('client-certificates/client/trusted/key.pem'),
}],
});
await page.goto(serverURL);
await expect(page.getByText('Playwright client-certificate error: self-signed certificate')).toBeVisible();
await page.close();
});
test.describe('persistentContext', () => {
test('validate input', async ({ launchPersistent }) => {
test.slow();