Skip to content

Commit afb8d3d

Browse files
committed
refactoring and cleanups
1 parent a4dad1a commit afb8d3d

File tree

3 files changed

+79
-53
lines changed

3 files changed

+79
-53
lines changed

packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts

Lines changed: 25 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -54,44 +54,22 @@ class ALPNCache {
5454
const result = new ManualPromise<string>();
5555
this._cache.set(cacheKey, result);
5656
result.then(success);
57-
if (!proxySocket) {
58-
createTLSSocket({
59-
host,
60-
port,
61-
servername: net.isIP(host) ? undefined : host,
62-
ALPNProtocols: ['h2', 'http/1.1'],
63-
rejectUnauthorized: false,
64-
secureContext
65-
}).then(socket => {
66-
// The server may not respond with ALPN, in which case we default to http/1.1.
67-
result.resolve(socket.alpnProtocol || 'http/1.1');
68-
socket.end();
69-
}).catch(error => {
70-
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
71-
result.resolve('http/1.1');
72-
});
73-
} else {
74-
const socket = tls.connect({
75-
socket: proxySocket,
76-
port: port,
77-
host: host,
78-
ALPNProtocols: ['h2', 'http/1.1'],
79-
rejectUnauthorized: false,
80-
secureContext: secureContext,
81-
servername: net.isIP(host) ? undefined : host
82-
});
83-
socket.on('secureConnect', () => {
84-
result.resolve(socket.alpnProtocol || 'http/1.1');
85-
socket.end();
86-
});
87-
socket.on('error', error => {
88-
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
89-
result.resolve('http/1.1');
90-
});
91-
socket.on('timeout', () => {
92-
result.resolve('http/1.1');
93-
});
94-
}
57+
createTLSSocket({
58+
socket: proxySocket,
59+
host,
60+
port,
61+
servername: net.isIP(host) ? undefined : host,
62+
ALPNProtocols: ['h2', 'http/1.1'],
63+
rejectUnauthorized: false,
64+
secureContext,
65+
}).then(socket => {
66+
// The server may not respond with ALPN, in which case we default to http/1.1.
67+
result.resolve(socket.alpnProtocol || 'http/1.1');
68+
socket.end();
69+
}).catch(error => {
70+
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
71+
result.resolve('http/1.1');
72+
});
9573
}
9674
}
9775

@@ -123,11 +101,7 @@ class SocksProxyConnection {
123101
}
124102

125103
async connect() {
126-
if (this.socksProxy.proxyAgentFromOptions)
127-
this.target = await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
128-
else
129-
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
130-
104+
this.target = await this._createProxySocket() ?? await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
131105
this.target.once('close', this._targetCloseEventListener);
132106
this.target.once('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
133107
if (this._closed) {
@@ -166,19 +140,21 @@ class SocksProxyConnection {
166140
this.target.write(data);
167141
}
168142

143+
private async _createProxySocket() {
144+
if (this.socksProxy.proxyAgentFromOptions)
145+
return await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
146+
}
147+
169148
private async _attachTLSListeners() {
170149
this.internal = new stream.Duplex({
171-
read: () => {},
150+
read: () => { },
172151
write: (data, encoding, callback) => {
173152
this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data });
174153
callback();
175154
}
176155
});
177156
const secureContext = this.socksProxy.secureContextMap.get(new URL(`https://${this.host}:${this.port}`).origin);
178-
let proxySocket: stream.Duplex | undefined = undefined;
179-
if (this.socksProxy.proxyAgentFromOptions)
180-
proxySocket = await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
181-
157+
const proxySocket = await this._createProxySocket();
182158
this.socksProxy.alpnCache.get(rewriteToLocalhostIfNeeded(this.host), this.port, secureContext, proxySocket, alpnProtocolChosenByServer => {
183159
proxySocket?.destroy();
184160
debugLogger.log('client-certificates', `Proxy->Target ${this.host}:${this.port} chooses ALPN ${alpnProtocolChosenByServer}`);
@@ -251,7 +227,7 @@ class SocksProxyConnection {
251227
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
252228
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
253229
servername: !net.isIP(this.host) ? this.host : undefined,
254-
secureContext: secureContext,
230+
secureContext,
255231
});
256232

257233
targetTLS.once('secureConnect', () => {

tests/config/proxy.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,16 @@ export class TestProxy {
133133
}
134134

135135
export async function setupSocksForwardingServer({
136-
port, forwardPort, allowedTargetPort
136+
port, forwardPort, allowedTargetPort, additionalAllowedHosts = []
137137
}: {
138-
port: number, forwardPort: number, allowedTargetPort: number
138+
port: number, forwardPort: number, allowedTargetPort: number, additionalAllowedHosts?: string[]
139139
}) {
140140
const connectHosts = [];
141141
const connections = new Map<string, net.Socket>();
142142
const socksProxy = new SocksProxy();
143143
socksProxy.setPattern('*');
144144
socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
145-
if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
145+
if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost', ...additionalAllowedHosts].includes(payload.host) || payload.port !== allowedTargetPort) {
146146
socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' });
147147
return;
148148
}

tests/library/client-certificates.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { expect, playwrightTest as base } from '../config/browserTest';
2424
import type net from 'net';
2525
import type { BrowserContextOptions } from 'packages/playwright-test';
2626
import { setupSocksForwardingServer } from '../config/proxy';
27+
import type { LookupAddress } from 'dns';
2728
const { createHttpsServer, createHttp2Server } = require('../../packages/playwright-core/lib/utils');
2829

2930
type TestOptions = {
@@ -391,6 +392,55 @@ test.describe('browser', () => {
391392
await page.close();
392393
});
393394

395+
test('should pass with matching certificates and when a socks proxy is used on an otherwise unreachable server', async ({ browser, startCCServer, asset, browserName, isMac }) => {
396+
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && isMac });
397+
const serverPort = parseInt(new URL(serverURL).port, 10);
398+
const privateDomain = `private.playwright.test`;
399+
const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({
400+
port: test.info().workerIndex + 2048 + 2,
401+
forwardPort: serverPort,
402+
allowedTargetPort: serverPort,
403+
additionalAllowedHosts: [privateDomain],
404+
});
405+
406+
// make private domain resolve to unreachable server 192.0.2.0
407+
// any attempt to connect will timeout
408+
let interceptedHostnameLookup: string | undefined;
409+
const __testHookLookup = (hostname: string): LookupAddress[] => {
410+
if (hostname === privateDomain) {
411+
interceptedHostnameLookup = hostname;
412+
return [
413+
{ address: '192.0.2.0', family: 4 },
414+
];
415+
}
416+
return [];
417+
};
418+
419+
const context = await browser.newContext({
420+
ignoreHTTPSErrors: true,
421+
clientCertificates: [{
422+
origin: new URL(serverURL).origin.replace('127.0.0.1', privateDomain),
423+
certPath: asset('client-certificates/client/trusted/cert.pem'),
424+
keyPath: asset('client-certificates/client/trusted/key.pem'),
425+
}],
426+
proxy: { server: proxyServerAddr },
427+
...
428+
{ __testHookLookup } as any
429+
});
430+
const page = await context.newPage();
431+
expect(connectHosts).toEqual([]);
432+
const requestURL = serverURL.replace('127.0.0.1', privateDomain);
433+
await page.goto(requestURL);
434+
435+
// only the proxy server should have tried to resolve the private domain
436+
// and the test proxy server does not resolve domains
437+
expect(interceptedHostnameLookup).toBe(undefined);
438+
expect(connectHosts).toEqual([`${privateDomain}:${serverPort}`]);
439+
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
440+
await page.close();
441+
await closeProxyServer();
442+
});
443+
394444
test('should pass with matching certificates and when a socks proxy is used', async ({ browser, startCCServer, asset, browserName, isMac }) => {
395445
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && isMac });
396446
const serverPort = parseInt(new URL(serverURL).port, 10);
@@ -791,7 +841,7 @@ test.describe('browser', () => {
791841
const page = await context.newPage();
792842

793843
// This was triggering an unhandled error before.
794-
await page.goto(serverUrl).catch(() => {});
844+
await page.goto(serverUrl).catch(() => { });
795845

796846
await context.close();
797847
await new Promise<void>(resolve => server.close(() => resolve()));

0 commit comments

Comments
 (0)