Skip to content

Commit

Permalink
chore: intercept socks proxy in the driver (#12021)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Feb 13, 2022
1 parent a0072af commit fb00991
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 289 deletions.
9 changes: 6 additions & 3 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
return await this._wrapApiCall(async () => {
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
let browser: Browser;
const { pipe } = await this._channel.connect({ wsEndpoint, headers: params.headers, slowMo: params.slowMo, timeout: params.timeout });
const connectParams: channels.BrowserTypeConnectParams = { wsEndpoint, headers: params.headers, slowMo: params.slowMo, timeout: params.timeout };
if ((params as any).__testHookPortForwarding) {
connectParams.enableSocksProxy = true;
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookPortForwarding.redirectPortForTest;
}
const { pipe } = await this._channel.connect(connectParams);
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection();
connection.markAsRemote();
Expand Down Expand Up @@ -168,8 +173,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
throw new Error('Malformed endpoint. Did you use launchServer method?');
}
playwright._setSelectors(this._playwright.selectors);
if ((params as any).__testHookPortForwarding)
playwright._enablePortForwarding((params as any).__testHookPortForwarding.redirectPortForTest);
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._logger = logger;
browser._shouldCloseConnectionOnClose = true;
Expand Down
6 changes: 6 additions & 0 deletions packages/playwright-core/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class Root extends ChannelOwner<channels.RootChannel> {
}
}

class DummyChannelOwner<T> extends ChannelOwner<T> {
}

export class Connection extends EventEmitter {
readonly _objects = new Map<string, ChannelOwner>();
onmessage = (message: object): void => {};
Expand Down Expand Up @@ -254,6 +257,9 @@ export class Connection extends EventEmitter {
case 'Selectors':
result = new SelectorsOwner(parent, type, guid, initializer);
break;
case 'SocksSupport':
result = new DummyChannelOwner(parent, type, guid, initializer);
break;
case 'Tracing':
result = new Tracing(parent, type, guid, initializer);
break;
Expand Down
65 changes: 17 additions & 48 deletions packages/playwright-core/src/client/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,9 @@
* limitations under the License.
*/

import dns from 'dns';
import net from 'net';
import util from 'util';
import * as channels from '../protocol/channels';
import { TimeoutError } from '../utils/errors';
import { createSocket } from '../utils/netUtils';
import * as socks from '../utils/socksProxy';
import { Android } from './android';
import { BrowserType } from './browserType';
import { ChannelOwner } from './channelOwner';
Expand All @@ -28,7 +25,6 @@ import { APIRequest } from './fetch';
import { LocalUtils } from './localUtils';
import { Selectors, SelectorsOwner } from './selectors';
import { Size } from './types';
const dnsLookupAsync = util.promisify(dns.lookup);

type DeviceDescriptor = {
userAgent: string,
Expand All @@ -51,8 +47,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
readonly request: APIRequest;
readonly errors: { TimeoutError: typeof TimeoutError };
_utils: LocalUtils;
private _sockets = new Map<string, net.Socket>();
private _redirectPortForTest: number | undefined;
private _socksProxyHandler: socks.SocksProxyHandler | undefined;

constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
super(parent, type, guid, initializer);
Expand All @@ -76,8 +71,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.selectors._addChannel(selectorsOwner);
this._connection.on('close', () => {
this.selectors._removeChannel(selectorsOwner);
for (const uid of this._sockets.keys())
this._onSocksClosed(uid);
this._socksProxyHandler?.cleanup();
});
(global as any)._playwrightInstance = this;
}
Expand All @@ -93,49 +87,24 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.selectors._addChannel(selectorsOwner);
}

// TODO: remove this methods together with PlaywrightClient.
_enablePortForwarding(redirectPortForTest?: number) {
this._redirectPortForTest = redirectPortForTest;
this._channel.on('socksRequested', ({ uid, host, port }) => this._onSocksRequested(uid, host, port));
this._channel.on('socksData', ({ uid, data }) => this._onSocksData(uid, Buffer.from(data, 'base64')));
this._channel.on('socksClosed', ({ uid }) => this._onSocksClosed(uid));
}

private async _onSocksRequested(uid: string, host: string, port: number): Promise<void> {
if (host === 'local.playwright')
host = 'localhost';
try {
if (this._redirectPortForTest)
port = this._redirectPortForTest;
const { address } = await dnsLookupAsync(host);
const socket = await createSocket(address, port);
socket.on('data', data => this._channel.socksData({ uid, data: data.toString('base64') }).catch(() => {}));
socket.on('error', error => {
this._channel.socksError({ uid, error: error.message }).catch(() => { });
this._sockets.delete(uid);
});
socket.on('end', () => {
this._channel.socksEnd({ uid }).catch(() => {});
this._sockets.delete(uid);
});
const localAddress = socket.localAddress;
const localPort = socket.localPort;
this._sockets.set(uid, socket);
this._channel.socksConnected({ uid, host: localAddress, port: localPort }).catch(() => {});
} catch (error) {
this._channel.socksFailed({ uid, errorCode: error.code }).catch(() => {});
}
}

private _onSocksData(uid: string, data: Buffer): void {
this._sockets.get(uid)?.write(data);
const socksSupport = this._initializer.socksSupport;
if (!socksSupport)
return;
const handler = new socks.SocksProxyHandler(redirectPortForTest);
this._socksProxyHandler = handler;
handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => socksSupport.socksConnected(payload).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => socksSupport.socksData({ uid: payload.uid, data: payload.data.toString('base64') }).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => socksSupport.socksError(payload).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => socksSupport.socksFailed(payload).catch(() => {}));
handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => socksSupport.socksEnd(payload).catch(() => {}));
socksSupport.on('socksRequested', payload => handler.socketRequested(payload));
socksSupport.on('socksClosed', payload => handler.socketClosed(payload));
socksSupport.on('socksData', payload => handler.sendSocketData({ uid: payload.uid, data: Buffer.from(payload.data, 'base64') }));
}

static from(channel: channels.PlaywrightChannel): Playwright {
return (channel as any)._object;
}

private _onSocksClosed(uid: string): void {
this._sockets.get(uid)?.destroy();
this._sockets.delete(uid);
}
}
72 changes: 69 additions & 3 deletions packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import WebSocket from 'ws';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import { getUserAgent, makeWaitForNextTask } from '../utils/utils';
import { ManualPromise } from '../utils/async';
import * as socks from '../utils/socksProxy';
import EventEmitter from 'events';

export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeChannel> implements channels.BrowserTypeChannel {
_type_BrowserType = true;
Expand Down Expand Up @@ -65,24 +67,36 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
headers: paramsHeaders,
followRedirects: true,
});
let socksInterceptor: SocksInterceptor | undefined;
const pipe = new JsonPipeDispatcher(this._scope);
const openPromise = new ManualPromise<{ pipe: JsonPipeDispatcher }>();
ws.on('open', () => openPromise.resolve({ pipe }));
ws.on('close', () => pipe.wasClosed());
ws.on('close', () => {
socksInterceptor?.cleanup();
pipe.wasClosed();
});
ws.on('error', error => {
socksInterceptor?.cleanup();
if (openPromise.isDone()) {
pipe.wasClosed(error);
} else {
pipe.dispose();
openPromise.reject(error);
}
});
pipe.on('close', () => ws.close());
pipe.on('close', () => {
socksInterceptor?.cleanup();
ws.close();
});
pipe.on('message', message => ws.send(JSON.stringify(message)));
ws.addEventListener('message', event => {
waitForNextTask(() => {
try {
pipe.dispatch(JSON.parse(event.data as string));
const json = JSON.parse(event.data as string);
if (params.enableSocksProxy && json.method === '__create__' && json.params.type === 'SocksSupport')
socksInterceptor = new SocksInterceptor(ws, params.socksProxyRedirectPortForTest, json.params.guid);
if (!socksInterceptor?.interceptMessage(json))
pipe.dispatch(json);
} catch (e) {
ws.close();
}
Expand All @@ -91,3 +105,55 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
return openPromise;
}
}

class SocksInterceptor {
private _handler: socks.SocksProxyHandler;
private _channel: channels.SocksSupportChannel & EventEmitter;
private _socksSupportObjectGuid: string;
private _ids = new Set<number>();

constructor(ws: WebSocket, redirectPortForTest: number | undefined, socksSupportObjectGuid: string) {
this._handler = new socks.SocksProxyHandler(redirectPortForTest);
this._socksSupportObjectGuid = socksSupportObjectGuid;

let lastId = -1;
this._channel = new Proxy(new EventEmitter(), {
get: (obj: any, prop) => {
if ((prop in obj) || obj[prop] !== undefined || typeof prop !== 'string')
return obj[prop];
return (params: any) => {
try {
const id = --lastId;
this._ids.add(id);
ws.send(JSON.stringify({ id, guid: socksSupportObjectGuid, method: prop, params, metadata: { stack: [], apiName: '', internal: true } }));
} catch (e) {
}
};
},
}) as channels.SocksSupportChannel & EventEmitter;
this._handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => this._channel.socksConnected(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => this._channel.socksData({ uid: payload.uid, data: payload.data.toString('base64') }));
this._handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => this._channel.socksError(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => this._channel.socksFailed(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => this._channel.socksEnd(payload));
this._channel.on('socksRequested', payload => this._handler.socketRequested(payload));
this._channel.on('socksClosed', payload => this._handler.socketClosed(payload));
this._channel.on('socksData', payload => this._handler.sendSocketData({ uid: payload.uid, data: Buffer.from(payload.data, 'base64') }));
}

cleanup() {
this._handler.cleanup();
}

interceptMessage(message: any): boolean {
if (this._ids.has(message.id)) {
this._ids.delete(message.id);
return true;
}
if (message.guid === this._socksSupportObjectGuid) {
this._channel.emit(message.method, message.params);
return true;
}
return false;
}
}
61 changes: 35 additions & 26 deletions packages/playwright-core/src/dispatchers/playwrightDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import * as channels from '../protocol/channels';
import { Browser } from '../server/browser';
import { GlobalAPIRequestContext } from '../server/fetch';
import { Playwright } from '../server/playwright';
import { SocksProxy } from '../server/socksProxy';
import { SocksProxy, SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../utils/socksProxy';
import * as types from '../server/types';
import { AndroidDispatcher } from './androidDispatcher';
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
Expand All @@ -28,11 +28,11 @@ import { LocalUtilsDispatcher } from './localUtilsDispatcher';
import { APIRequestContextDispatcher } from './networkDispatchers';
import { SelectorsDispatcher } from './selectorsDispatcher';
import { ConnectedBrowserDispatcher } from './browserDispatcher';
import { createGuid } from '../utils/utils';

export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightChannel> implements channels.PlaywrightChannel {
_type_Playwright;
private _browserDispatcher: ConnectedBrowserDispatcher | undefined;
private _socksProxy: SocksProxy | undefined;

constructor(scope: DispatcherScope, playwright: Playwright, socksProxy?: SocksProxy, preLaunchedBrowser?: Browser) {
const descriptors = require('../server/deviceDescriptors') as types.Devices;
Expand All @@ -49,48 +49,57 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
deviceDescriptors,
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
preLaunchedBrowser: browserDispatcher,
socksSupport: socksProxy ? new SocksSupportDispatcher(scope, socksProxy) : undefined,
}, false);
this._type_Playwright = true;
this._browserDispatcher = browserDispatcher;
if (socksProxy) {
this._socksProxy = socksProxy;
socksProxy.on(SocksProxy.Events.SocksRequested, data => this._dispatchEvent('socksRequested', data));
socksProxy.on(SocksProxy.Events.SocksData, data => this._dispatchEvent('socksData', data));
socksProxy.on(SocksProxy.Events.SocksClosed, data => this._dispatchEvent('socksClosed', data));
}
}

async socksConnected(params: channels.PlaywrightSocksConnectedParams): Promise<void> {
this._socksProxy?.socketConnected(params.uid, params.host, params.port);
async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise<channels.PlaywrightNewRequestResult> {
const request = new GlobalAPIRequestContext(this._object, params);
return { request: APIRequestContextDispatcher.from(this._scope, request) };
}

async hideHighlight(params: channels.PlaywrightHideHighlightParams, metadata?: channels.Metadata): Promise<channels.PlaywrightHideHighlightResult> {
await this._object.hideHighlight();
}

async socksFailed(params: channels.PlaywrightSocksFailedParams): Promise<void> {
this._socksProxy?.socketFailed(params.uid, params.errorCode);
async cleanup() {
// Cleanup contexts upon disconnect.
await this._browserDispatcher?.cleanupContexts();
}
}

class SocksSupportDispatcher extends Dispatcher<{ guid: string }, channels.SocksSupportChannel> implements channels.SocksSupportChannel {
_type_SocksSupport: boolean;
private _socksProxy: SocksProxy;

async socksData(params: channels.PlaywrightSocksDataParams): Promise<void> {
this._socksProxy?.sendSocketData(params.uid, Buffer.from(params.data, 'base64'));
constructor(scope: DispatcherScope, socksProxy: SocksProxy) {
super(scope, { guid: 'socksSupport@' + createGuid() }, 'SocksSupport', {});
this._type_SocksSupport = true;
this._socksProxy = socksProxy;
socksProxy.on(SocksProxy.Events.SocksRequested, (payload: SocksSocketRequestedPayload) => this._dispatchEvent('socksRequested', payload));
socksProxy.on(SocksProxy.Events.SocksData, (payload: SocksSocketDataPayload) => this._dispatchEvent('socksData', { uid: payload.uid, data: payload.data.toString('base64') }));
socksProxy.on(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => this._dispatchEvent('socksClosed', payload));
}

async socksError(params: channels.PlaywrightSocksErrorParams): Promise<void> {
this._socksProxy?.sendSocketError(params.uid, params.error);
async socksConnected(params: channels.SocksSupportSocksConnectedParams): Promise<void> {
this._socksProxy?.socketConnected(params);
}

async socksEnd(params: channels.PlaywrightSocksEndParams): Promise<void> {
this._socksProxy?.sendSocketEnd(params.uid);
async socksFailed(params: channels.SocksSupportSocksFailedParams): Promise<void> {
this._socksProxy?.socketFailed(params);
}

async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise<channels.PlaywrightNewRequestResult> {
const request = new GlobalAPIRequestContext(this._object, params);
return { request: APIRequestContextDispatcher.from(this._scope, request) };
async socksData(params: channels.SocksSupportSocksDataParams): Promise<void> {
this._socksProxy?.sendSocketData({ uid: params.uid, data: Buffer.from(params.data, 'base64') });
}

async hideHighlight(params: channels.PlaywrightHideHighlightParams, metadata?: channels.Metadata): Promise<channels.PlaywrightHideHighlightResult> {
await this._object.hideHighlight();
async socksError(params: channels.SocksSupportSocksErrorParams): Promise<void> {
this._socksProxy?.sendSocketError(params);
}

async cleanup() {
// Cleanup contexts upon disconnect.
await this._browserDispatcher?.cleanupContexts();
async socksEnd(params: channels.SocksSupportSocksEndParams): Promise<void> {
this._socksProxy?.sendSocketEnd(params);
}
}
2 changes: 1 addition & 1 deletion packages/playwright-core/src/grid/gridWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { DispatcherConnection, Root } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { createPlaywright } from '../server/playwright';
import { gracefullyCloseAll } from '../utils/processLauncher';
import { SocksProxy } from '../server/socksProxy';
import { SocksProxy } from '../utils/socksProxy';

function launchGridWorker(gridURL: string, agentId: string, workerId: string) {
const log = debug(`pw:grid:worker${workerId}`);
Expand Down

0 comments on commit fb00991

Please sign in to comment.