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 _sockets = new Set<net.Socket>();
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
private _port: number | undefined;
|
private _port: number | undefined;
|
||||||
|
private _pattern: string | undefined;
|
||||||
|
private _directSockets = new Map<string, net.Socket>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
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() {
|
port() {
|
||||||
return this._port;
|
return this._port;
|
||||||
}
|
}
|
||||||
|
|
@ -339,14 +372,29 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSocketRequested(payload: SocksSocketRequestedPayload) {
|
onSocketRequested(payload: SocksSocketRequestedPayload) {
|
||||||
|
if (!this._matchesPattern(payload)) {
|
||||||
|
this._handleDirect(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.emit(SocksProxy.Events.SocksRequested, payload);
|
this.emit(SocksProxy.Events.SocksRequested, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSocketData(payload: SocksSocketDataPayload): void {
|
onSocketData(payload: SocksSocketDataPayload): void {
|
||||||
|
const direct = this._directSockets.get(payload.uid);
|
||||||
|
if (direct) {
|
||||||
|
direct.write(payload.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.emit(SocksProxy.Events.SocksData, payload);
|
this.emit(SocksProxy.Events.SocksData, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSocketClosed(payload: SocksSocketClosedPayload): void {
|
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);
|
this.emit(SocksProxy.Events.SocksClosed, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ async function tetherHostNetwork(endpoint: string) {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
'User-Agent': getUserAgent(),
|
'User-Agent': getUserAgent(),
|
||||||
'x-playwright-network-tethering': '1',
|
'x-playwright-network-tethering': '1',
|
||||||
|
'x-playwright-proxy': '*',
|
||||||
};
|
};
|
||||||
const transport = await WebSocketTransport.connect(undefined /* progress */, wsEndpoint, headers, true /* followRedirects */);
|
const transport = await WebSocketTransport.connect(undefined /* progress */, wsEndpoint, headers, true /* followRedirects */);
|
||||||
const socksInterceptor = new SocksInterceptor(transport, undefined);
|
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}`);
|
const log = debug(`pw:grid:worker:${workerId}`);
|
||||||
log('created');
|
log('created');
|
||||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
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');
|
log('exiting process');
|
||||||
setTimeout(() => process.exit(0), 30000);
|
setTimeout(() => process.exit(0), 30000);
|
||||||
// Meanwhile, try to gracefully close all browsers.
|
// 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';
|
export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser' | 'network-tethering';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
enableSocksProxy: boolean,
|
socksProxy: string | undefined,
|
||||||
browserName: string | null,
|
browserName: string | null,
|
||||||
launchOptions: LaunchOptions,
|
launchOptions: LaunchOptions,
|
||||||
};
|
};
|
||||||
|
|
@ -99,6 +99,7 @@ export class PlaywrightConnection {
|
||||||
private async _initPlaywrightTetheringMode(scope: RootDispatcher) {
|
private async _initPlaywrightTetheringMode(scope: RootDispatcher) {
|
||||||
this._debugLog(`engaged playwright.tethering mode`);
|
this._debugLog(`engaged playwright.tethering mode`);
|
||||||
const playwright = createPlaywright('javascript');
|
const playwright = createPlaywright('javascript');
|
||||||
|
this._preLaunched.networkTetheringSocksProxy?.setProxyPattern(this._options.socksProxy);
|
||||||
return new PlaywrightDispatcher(scope, playwright, this._preLaunched.networkTetheringSocksProxy);
|
return new PlaywrightDispatcher(scope, playwright, this._preLaunched.networkTetheringSocksProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +222,7 @@ export class PlaywrightConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _configureSocksProxy(playwright: Playwright): Promise<undefined|SocksProxy> {
|
private async _configureSocksProxy(playwright: Playwright): Promise<undefined|SocksProxy> {
|
||||||
if (!this._options.enableSocksProxy)
|
if (!this._options.socksProxy)
|
||||||
return undefined;
|
return undefined;
|
||||||
if (this._preLaunched.networkTetheringSocksProxy) {
|
if (this._preLaunched.networkTetheringSocksProxy) {
|
||||||
playwright.options.socksProxyPort = this._preLaunched.networkTetheringSocksProxy.port();
|
playwright.options.socksProxyPort = this._preLaunched.networkTetheringSocksProxy.port();
|
||||||
|
|
@ -229,6 +230,7 @@ export class PlaywrightConnection {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const socksProxy = new SocksProxy();
|
const socksProxy = new SocksProxy();
|
||||||
|
socksProxy.setProxyPattern(this._options.socksProxy);
|
||||||
playwright.options.socksProxyPort = await socksProxy.listen(0);
|
playwright.options.socksProxyPort = await socksProxy.listen(0);
|
||||||
this._debugLog(`started socks proxy on port ${playwright.options.socksProxyPort}`);
|
this._debugLog(`started socks proxy on port ${playwright.options.socksProxyPort}`);
|
||||||
this._cleanups.push(() => socksProxy.close());
|
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 browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
|
||||||
const proxyHeader = request.headers['x-playwright-proxy'];
|
const proxyHeader = request.headers['x-playwright-proxy'];
|
||||||
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
|
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'] || '';
|
const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
|
||||||
let launchOptions: LaunchOptions = {};
|
let launchOptions: LaunchOptions = {};
|
||||||
|
|
@ -160,7 +160,7 @@ export class PlaywrightServer {
|
||||||
const connection = new PlaywrightConnection(
|
const connection = new PlaywrightConnection(
|
||||||
semaphore.aquire(),
|
semaphore.aquire(),
|
||||||
clientType, ws,
|
clientType, ws,
|
||||||
{ enableSocksProxy, browserName, launchOptions },
|
{ socksProxy, browserName, launchOptions },
|
||||||
{
|
{
|
||||||
playwright: this._preLaunchedPlaywright,
|
playwright: this._preLaunchedPlaywright,
|
||||||
browser: this._options.preLaunchedBrowser,
|
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) => {
|
pageFactory: async ({ browserType, browserName, channel }, run, testInfo) => {
|
||||||
const playwrightServers: OutOfProcessPlaywrightServer[] = [];
|
const playwrightServers: OutOfProcessPlaywrightServer[] = [];
|
||||||
const browsers: Browser[] = [];
|
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);
|
const server = new OutOfProcessPlaywrightServer(0, 3200 + testInfo.workerIndex);
|
||||||
playwrightServers.push(server);
|
playwrightServers.push(server);
|
||||||
const browser = await browserType.connect({
|
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 }) },
|
headers: { 'x-playwright-launch-options': JSON.stringify({ channel }) },
|
||||||
__testHookRedirectPortForwarding: redirectPortForTest,
|
__testHookRedirectPortForwarding: redirectPortForTest,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
@ -166,5 +166,37 @@ it('should lead to the error page for forwarded requests when the connection is
|
||||||
else if (browserName === 'webkit')
|
else if (browserName === 'webkit')
|
||||||
expect(error.message).toBeTruthy();
|
expect(error.message).toBeTruthy();
|
||||||
else if (browserName === 'firefox')
|
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