feat(tether): support hostname pattern in socks proxy (#19302)
For now, only '*' and 'localhost' are supported. References #19287.
This commit is contained in:
parent
cd4ccdfa29
commit
fd22d8bde1
|
|
@ -297,6 +297,8 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
|||
private _sockets = new Set<net.Socket>();
|
||||
private _closed = false;
|
||||
private _port: number | undefined;
|
||||
private _pattern: string | undefined;
|
||||
private _directSockets = new Map<string, net.Socket>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -315,6 +317,37 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
|||
});
|
||||
}
|
||||
|
||||
setProxyPattern(pattern: string | undefined) {
|
||||
this._pattern = pattern;
|
||||
}
|
||||
|
||||
private _matchesPattern(request: SocksSocketRequestedPayload) {
|
||||
return this._pattern === '*' || (this._pattern === 'localhost' && request.host === 'localhost');
|
||||
}
|
||||
|
||||
private async _handleDirect(request: SocksSocketRequestedPayload) {
|
||||
try {
|
||||
// TODO: Node.js 17 does resolve localhost to ipv6
|
||||
const { address } = await dnsLookupAsync(request.host === 'localhost' ? '127.0.0.1' : request.host);
|
||||
const socket = await createSocket(address, request.port);
|
||||
socket.on('data', data => this._connections.get(request.uid)?.sendData(data));
|
||||
socket.on('error', error => {
|
||||
this._connections.get(request.uid)?.error(error.message);
|
||||
this._directSockets.delete(request.uid);
|
||||
});
|
||||
socket.on('end', () => {
|
||||
this._connections.get(request.uid)?.end();
|
||||
this._directSockets.delete(request.uid);
|
||||
});
|
||||
const localAddress = socket.localAddress;
|
||||
const localPort = socket.localPort;
|
||||
this._directSockets.set(request.uid, socket);
|
||||
this._connections.get(request.uid)?.socketConnected(localAddress, localPort);
|
||||
} catch (error) {
|
||||
this._connections.get(request.uid)?.socketFailed(error.code);
|
||||
}
|
||||
}
|
||||
|
||||
port() {
|
||||
return this._port;
|
||||
}
|
||||
|
|
@ -339,14 +372,29 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
|||
}
|
||||
|
||||
onSocketRequested(payload: SocksSocketRequestedPayload) {
|
||||
if (!this._matchesPattern(payload)) {
|
||||
this._handleDirect(payload);
|
||||
return;
|
||||
}
|
||||
this.emit(SocksProxy.Events.SocksRequested, payload);
|
||||
}
|
||||
|
||||
onSocketData(payload: SocksSocketDataPayload): void {
|
||||
const direct = this._directSockets.get(payload.uid);
|
||||
if (direct) {
|
||||
direct.write(payload.data);
|
||||
return;
|
||||
}
|
||||
this.emit(SocksProxy.Events.SocksData, payload);
|
||||
}
|
||||
|
||||
onSocketClosed(payload: SocksSocketClosedPayload): void {
|
||||
const direct = this._directSockets.get(payload.uid);
|
||||
if (direct) {
|
||||
direct.destroy();
|
||||
this._directSockets.delete(payload.uid);
|
||||
return;
|
||||
}
|
||||
this.emit(SocksProxy.Events.SocksClosed, payload);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ async function tetherHostNetwork(endpoint: string) {
|
|||
const headers: any = {
|
||||
'User-Agent': getUserAgent(),
|
||||
'x-playwright-network-tethering': '1',
|
||||
'x-playwright-proxy': '*',
|
||||
};
|
||||
const transport = await WebSocketTransport.connect(undefined /* progress */, wsEndpoint, headers, true /* followRedirects */);
|
||||
const socksInterceptor = new SocksInterceptor(transport, undefined);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
|
|||
const log = debug(`pw:grid:worker:${workerId}`);
|
||||
log('created');
|
||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
||||
new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { enableSocksProxy: true, browserName, launchOptions: {} }, { }, log, async () => {
|
||||
new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { socksProxy: '*', browserName, launchOptions: {} }, { }, log, async () => {
|
||||
log('exiting process');
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
// Meanwhile, try to gracefully close all browsers.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { DebugControllerDispatcher } from '../server/dispatchers/debugController
|
|||
export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser' | 'network-tethering';
|
||||
|
||||
type Options = {
|
||||
enableSocksProxy: boolean,
|
||||
socksProxy: string | undefined,
|
||||
browserName: string | null,
|
||||
launchOptions: LaunchOptions,
|
||||
};
|
||||
|
|
@ -99,6 +99,7 @@ export class PlaywrightConnection {
|
|||
private async _initPlaywrightTetheringMode(scope: RootDispatcher) {
|
||||
this._debugLog(`engaged playwright.tethering mode`);
|
||||
const playwright = createPlaywright('javascript');
|
||||
this._preLaunched.networkTetheringSocksProxy?.setProxyPattern(this._options.socksProxy);
|
||||
return new PlaywrightDispatcher(scope, playwright, this._preLaunched.networkTetheringSocksProxy);
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ export class PlaywrightConnection {
|
|||
}
|
||||
|
||||
private async _configureSocksProxy(playwright: Playwright): Promise<undefined|SocksProxy> {
|
||||
if (!this._options.enableSocksProxy)
|
||||
if (!this._options.socksProxy)
|
||||
return undefined;
|
||||
if (this._preLaunched.networkTetheringSocksProxy) {
|
||||
playwright.options.socksProxyPort = this._preLaunched.networkTetheringSocksProxy.port();
|
||||
|
|
@ -229,6 +230,7 @@ export class PlaywrightConnection {
|
|||
return undefined;
|
||||
}
|
||||
const socksProxy = new SocksProxy();
|
||||
socksProxy.setProxyPattern(this._options.socksProxy);
|
||||
playwright.options.socksProxyPort = await socksProxy.listen(0);
|
||||
this._debugLog(`started socks proxy on port ${playwright.options.socksProxyPort}`);
|
||||
this._cleanups.push(() => socksProxy.close());
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export class PlaywrightServer {
|
|||
const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
|
||||
const proxyHeader = request.headers['x-playwright-proxy'];
|
||||
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
|
||||
const enableSocksProxy = this._options.browserProxyMode !== 'disabled' && proxyValue === '*';
|
||||
const socksProxy = this._options.browserProxyMode !== 'disabled' ? proxyValue : undefined;
|
||||
|
||||
const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
|
||||
let launchOptions: LaunchOptions = {};
|
||||
|
|
@ -160,7 +160,7 @@ export class PlaywrightServer {
|
|||
const connection = new PlaywrightConnection(
|
||||
semaphore.aquire(),
|
||||
clientType, ws,
|
||||
{ enableSocksProxy, browserName, launchOptions },
|
||||
{ socksProxy, browserName, launchOptions },
|
||||
{
|
||||
playwright: this._preLaunchedPlaywright,
|
||||
browser: this._options.preLaunchedBrowser,
|
||||
|
|
|
|||
|
|
@ -58,15 +58,15 @@ class OutOfProcessPlaywrightServer {
|
|||
}
|
||||
}
|
||||
|
||||
const it = contextTest.extend<{ pageFactory: (redirectPortForTest?: number) => Promise<Page> }>({
|
||||
const it = contextTest.extend<{ pageFactory: (redirectPortForTest?: number, pattern?: string) => Promise<Page> }>({
|
||||
pageFactory: async ({ browserType, browserName, channel }, run, testInfo) => {
|
||||
const playwrightServers: OutOfProcessPlaywrightServer[] = [];
|
||||
const browsers: Browser[] = [];
|
||||
await run(async (redirectPortForTest?: number): Promise<Page> => {
|
||||
await run(async (redirectPortForTest?: number, pattern = '*'): Promise<Page> => {
|
||||
const server = new OutOfProcessPlaywrightServer(0, 3200 + testInfo.workerIndex);
|
||||
playwrightServers.push(server);
|
||||
const browser = await browserType.connect({
|
||||
wsEndpoint: await server.wsEndpoint() + '?proxy=*&browser=' + browserName,
|
||||
wsEndpoint: await server.wsEndpoint() + `?proxy=${pattern}&browser=` + browserName,
|
||||
headers: { 'x-playwright-launch-options': JSON.stringify({ channel }) },
|
||||
__testHookRedirectPortForwarding: redirectPortForTest,
|
||||
} as any);
|
||||
|
|
@ -166,5 +166,37 @@ it('should lead to the error page for forwarded requests when the connection is
|
|||
else if (browserName === 'webkit')
|
||||
expect(error.message).toBeTruthy();
|
||||
else if (browserName === 'firefox')
|
||||
expect(error.message.includes('NS_ERROR_NET_RESET') || error.message.includes('NS_ERROR_CONNECTION_REFUSED')).toBe(true);
|
||||
expect(error.message.includes('NS_ERROR_NET_RESET') || error.message.includes('NS_ERROR_CONNECTION_REFUSED')).toBe(true);
|
||||
});
|
||||
|
||||
it('should proxy based on the pattern', async ({ pageFactory, server, browserName, platform }, workerInfo) => {
|
||||
it.skip(browserName === 'webkit' && platform === 'darwin');
|
||||
|
||||
const { testServerPort, stopTestServer } = await startTestServer();
|
||||
let reachedOriginalTarget = false;
|
||||
server.setRoute('/foo.html', async (req, res) => {
|
||||
reachedOriginalTarget = true;
|
||||
res.end('<html><body>from-original-server</body></html>');
|
||||
});
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const page = await pageFactory(testServerPort, 'localhost');
|
||||
|
||||
// localhost should be proxied.
|
||||
await page.goto(`http://localhost:${examplePort}/foo.html`);
|
||||
expect(await page.content()).toContain('from-retargeted-server');
|
||||
expect(reachedOriginalTarget).toBe(false);
|
||||
|
||||
// 127.0.0.1 should be served directly.
|
||||
await page.goto(`http://127.0.0.1:${server.PORT}/foo.html`);
|
||||
expect(await page.content()).toContain('from-original-server');
|
||||
expect(reachedOriginalTarget).toBe(true);
|
||||
|
||||
// Random domain should be served directly and fail.
|
||||
let failed = false;
|
||||
await page.goto(`http://does-not-exist-bad-domain.oh-no-should-not-work`).catch(e => {
|
||||
failed = true;
|
||||
});
|
||||
expect(failed).toBe(true);
|
||||
|
||||
stopTestServer();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue