Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,11 @@ class SocksProxyConnection {
// the protocol on the first package and attach appropriate listeners.
if (!this._firstPackageReceived) {
this._firstPackageReceived = true;
// 0x16 is SSLv3/TLS "handshake" content type: https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record
if (data[0] === 0x16)
this._establishTlsTunnel(this._browserEncrypted, data);
// 0x16 is the TLS "handshake" content type. Only intercept it when the origin has a client
// certificate; otherwise pass the connection through so the browser talks TLS to the server directly.
const secureContext = data[0] === 0x16 ? this.socksProxy.secureContextMap.get(normalizeOrigin(`https://${this.host}:${this.port}`)) : undefined;
if (secureContext)
this._establishTlsTunnel(this._browserEncrypted, data, secureContext);
else
this._establishPlaintextTunnel(this._browserEncrypted);
}
Expand All @@ -176,14 +178,11 @@ class SocksProxyConnection {
this._serverEncrypted.pipe(browserEncrypted);
}

private _establishTlsTunnel(browserEncrypted: stream.Duplex, clientHello: Buffer) {
private _establishTlsTunnel(browserEncrypted: stream.Duplex, clientHello: Buffer, secureContext: tls.SecureContext) {
const browserALPNProtocols = parseALPNFromClientHello(clientHello) || ['http/1.1'];
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} offers ALPN ${browserALPNProtocols}`);

const secureContext = this.socksProxy.secureContextMap.get(normalizeOrigin(`https://${this.host}:${this.port}`));
// Without a matching client certificate the proxy is a transparent pass-through, so let the
// browser validate the server cert instead of failing here on e.g. self-signed certs.
const rejectUnauthorized = !!secureContext && !this.socksProxy.ignoreHTTPSErrors;
const rejectUnauthorized = !this.socksProxy.ignoreHTTPSErrors;

const serverDecrypted = tls.connect({
socket: this._serverEncrypted,
Expand Down
24 changes: 21 additions & 3 deletions tests/library/client-certificates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,24 @@ test.describe('browser', () => {
await page.close();
});

test('should not intercept TLS for origins without a client certificate', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41106' },
}, async ({ browser, asset, httpsServer }) => {
// If the proxy intercepted this origin, the browser would see its self-signed cert (CN=localhost)
// instead of the real server cert (CN=playwright-test).
const page = await browser.newPage({
clientCertificates: [{
origin: 'https://not-matching.com',
certPath: asset('client-certificates/client/trusted/cert.pem'),
keyPath: asset('client-certificates/client/trusted/key.pem'),
}],
});
const response = await page.goto(httpsServer.EMPTY_PAGE);
expect(response.ok()).toBe(true);
expect((await response.securityDetails()).subjectName).toBe('playwright-test');
await page.close();
});

test('should fail with no client certificates', async ({ browser, startCCServer, asset, browserName, isMac }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && isMac });
const page = await browser.newPage({
Expand Down Expand Up @@ -636,13 +654,13 @@ test.describe('browser', () => {

await new Promise<void>(resolve => server.listen(0, 'localhost', resolve));
const port = (server.address() as net.AddressInfo).port;
const origin = 'https://' + (browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost');
const serverUrl = `${origin}:${port}`;
const host = browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost';
const serverUrl = `https://${host}:${port}`;

const context = await browser.newContext({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin,
origin: serverUrl,
certPath: asset('client-certificates/client/trusted/cert.pem'),
keyPath: asset('client-certificates/client/trusted/key.pem'),
}],
Expand Down
Loading