fix(client-certificates): return target errors on response when using http2 (#31867)
This commit is contained in:
parent
335d31bf65
commit
09581b615d
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import http2 from 'http2';
|
||||||
import type https from 'https';
|
import type https from 'https';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import tls from 'tls';
|
import tls from 'tls';
|
||||||
|
|
@ -80,17 +81,19 @@ class SocksProxyConnection {
|
||||||
target!: net.Socket;
|
target!: net.Socket;
|
||||||
// In case of http, we just pipe data to the target socket and they are |undefined|.
|
// In case of http, we just pipe data to the target socket and they are |undefined|.
|
||||||
internal: stream.Duplex | undefined;
|
internal: stream.Duplex | undefined;
|
||||||
|
private _targetCloseEventListener: () => void;
|
||||||
|
|
||||||
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
|
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
|
||||||
this.socksProxy = socksProxy;
|
this.socksProxy = socksProxy;
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
this._targetCloseEventListener = () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid });
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
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.target.on('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
|
||||||
this.socksProxy._socksProxy.socketConnected({
|
this.socksProxy._socksProxy.socketConnected({
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
|
|
@ -139,7 +142,6 @@ class SocksProxyConnection {
|
||||||
dummyServer.emit('connection', this.internal);
|
dummyServer.emit('connection', this.internal);
|
||||||
dummyServer.on('secureConnection', internalTLS => {
|
dummyServer.on('secureConnection', internalTLS => {
|
||||||
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`);
|
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 = {
|
const tlsOptions: tls.ConnectionOptions = {
|
||||||
socket: this.target,
|
socket: this.target,
|
||||||
host: this.host,
|
host: this.host,
|
||||||
|
|
@ -154,8 +156,10 @@ class SocksProxyConnection {
|
||||||
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
||||||
const targetTLS = tls.connect(tlsOptions);
|
const targetTLS = tls.connect(tlsOptions);
|
||||||
|
|
||||||
internalTLS.pipe(targetTLS);
|
targetTLS.on('secureConnect', () => {
|
||||||
targetTLS.pipe(internalTLS);
|
internalTLS.pipe(targetTLS);
|
||||||
|
targetTLS.pipe(internalTLS);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle close and errors
|
// Handle close and errors
|
||||||
const closeBothSockets = () => {
|
const closeBothSockets = () => {
|
||||||
|
|
@ -169,11 +173,30 @@ class SocksProxyConnection {
|
||||||
internalTLS.on('error', () => closeBothSockets());
|
internalTLS.on('error', () => closeBothSockets());
|
||||||
targetTLS.on('error', error => {
|
targetTLS.on('error', error => {
|
||||||
debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`);
|
debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`);
|
||||||
|
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
||||||
if (internalTLS?.alpnProtocol === 'h2') {
|
if (internalTLS?.alpnProtocol === 'h2') {
|
||||||
// https://github.com/nodejs/node/issues/46152
|
// This method is available only in Node.js 20+
|
||||||
// TODO: http2.performServerHandshake does not work here for some reason.
|
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 {
|
} else {
|
||||||
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
|
||||||
internalTLS.end([
|
internalTLS.end([
|
||||||
'HTTP/1.1 503 Internal Server Error',
|
'HTTP/1.1 503 Internal Server Error',
|
||||||
'Content-Type: text/html; charset=utf-8',
|
'Content-Type: text/html; charset=utf-8',
|
||||||
|
|
@ -181,8 +204,8 @@ class SocksProxyConnection {
|
||||||
'\r\n',
|
'\r\n',
|
||||||
responseBody,
|
responseBody,
|
||||||
].join('\r\n'));
|
].join('\r\n'));
|
||||||
|
closeBothSockets();
|
||||||
}
|
}
|
||||||
closeBothSockets();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const { createHttpsServer, createHttp2Server } = require('../../packages/playwri
|
||||||
type TestOptions = {
|
type TestOptions = {
|
||||||
startCCServer(options?: {
|
startCCServer(options?: {
|
||||||
http2?: boolean;
|
http2?: boolean;
|
||||||
|
enableHTTP1FallbackWhenUsingHttp2?: boolean;
|
||||||
useFakeLocalhost?: boolean;
|
useFakeLocalhost?: boolean;
|
||||||
}): Promise<string>,
|
}): Promise<string>,
|
||||||
};
|
};
|
||||||
|
|
@ -42,7 +43,7 @@ const test = base.extend<TestOptions>({
|
||||||
],
|
],
|
||||||
requestCert: true,
|
requestCert: true,
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
allowHTTP1: true,
|
allowHTTP1: options?.enableHTTP1FallbackWhenUsingHttp2,
|
||||||
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
|
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
|
||||||
const tlsSocket = req.socket as import('tls').TLSSocket;
|
const tlsSocket = req.socket as import('tls').TLSSocket;
|
||||||
const parts: { key: string, value: any }[] = [];
|
const parts: { key: string, value: any }[] = [];
|
||||||
|
|
@ -279,7 +280,8 @@ test.describe('browser', () => {
|
||||||
|
|
||||||
test('support http2', async ({ browser, startCCServer, asset, browserName }) => {
|
test('support http2', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost');
|
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({
|
const page = await browser.newPage({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
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.
|
// TODO: We should investigate why http2 is not supported in WebKit on Linux.
|
||||||
// https://bugs.webkit.org/show_bug.cgi?id=276990
|
// 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 page.goto(serverURL.replace('localhost', 'local.playwright'));
|
||||||
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
|
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('support http2 if the browser only supports http1.1', async ({ browserType, browserName, startCCServer, asset }) => {
|
||||||
test.skip(browserName !== 'chromium');
|
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 browser = await browserType.launch({ args: ['--disable-http2'] });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
|
|
@ -328,6 +330,25 @@ test.describe('browser', () => {
|
||||||
await browser.close();
|
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.describe('persistentContext', () => {
|
||||||
test('validate input', async ({ launchPersistent }) => {
|
test('validate input', async ({ launchPersistent }) => {
|
||||||
test.slow();
|
test.slow();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue