chore: intercept socks proxy in the driver (#12021)

This commit is contained in:
Dmitry Gozman 2022-02-13 14:03:47 -08:00 committed by GitHub
parent a0072af2f3
commit fb00991a78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 432 additions and 291 deletions

View file

@ -128,7 +128,12 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
return await this._wrapApiCall(async () => { return await this._wrapApiCall(async () => {
const deadline = params.timeout ? monotonicTime() + params.timeout : 0; const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
let browser: Browser; 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 closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(); const connection = new Connection();
connection.markAsRemote(); connection.markAsRemote();
@ -168,8 +173,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
throw new Error('Malformed endpoint. Did you use launchServer method?'); throw new Error('Malformed endpoint. Did you use launchServer method?');
} }
playwright._setSelectors(this._playwright.selectors); playwright._setSelectors(this._playwright.selectors);
if ((params as any).__testHookPortForwarding)
playwright._enablePortForwarding((params as any).__testHookPortForwarding.redirectPortForTest);
browser = Browser.from(playwright._initializer.preLaunchedBrowser!); browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._logger = logger; browser._logger = logger;
browser._shouldCloseConnectionOnClose = true; browser._shouldCloseConnectionOnClose = true;

View file

@ -55,6 +55,9 @@ class Root extends ChannelOwner<channels.RootChannel> {
} }
} }
class DummyChannelOwner<T> extends ChannelOwner<T> {
}
export class Connection extends EventEmitter { export class Connection extends EventEmitter {
readonly _objects = new Map<string, ChannelOwner>(); readonly _objects = new Map<string, ChannelOwner>();
onmessage = (message: object): void => {}; onmessage = (message: object): void => {};
@ -254,6 +257,9 @@ export class Connection extends EventEmitter {
case 'Selectors': case 'Selectors':
result = new SelectorsOwner(parent, type, guid, initializer); result = new SelectorsOwner(parent, type, guid, initializer);
break; break;
case 'SocksSupport':
result = new DummyChannelOwner(parent, type, guid, initializer);
break;
case 'Tracing': case 'Tracing':
result = new Tracing(parent, type, guid, initializer); result = new Tracing(parent, type, guid, initializer);
break; break;

View file

@ -14,12 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import dns from 'dns';
import net from 'net';
import util from 'util';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { TimeoutError } from '../utils/errors'; import { TimeoutError } from '../utils/errors';
import { createSocket } from '../utils/netUtils'; import * as socks from '../utils/socksProxy';
import { Android } from './android'; import { Android } from './android';
import { BrowserType } from './browserType'; import { BrowserType } from './browserType';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
@ -28,7 +25,6 @@ import { APIRequest } from './fetch';
import { LocalUtils } from './localUtils'; import { LocalUtils } from './localUtils';
import { Selectors, SelectorsOwner } from './selectors'; import { Selectors, SelectorsOwner } from './selectors';
import { Size } from './types'; import { Size } from './types';
const dnsLookupAsync = util.promisify(dns.lookup);
type DeviceDescriptor = { type DeviceDescriptor = {
userAgent: string, userAgent: string,
@ -51,8 +47,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
readonly request: APIRequest; readonly request: APIRequest;
readonly errors: { TimeoutError: typeof TimeoutError }; readonly errors: { TimeoutError: typeof TimeoutError };
_utils: LocalUtils; _utils: LocalUtils;
private _sockets = new Map<string, net.Socket>(); private _socksProxyHandler: socks.SocksProxyHandler | undefined;
private _redirectPortForTest: number | undefined;
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
@ -76,8 +71,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.selectors._addChannel(selectorsOwner); this.selectors._addChannel(selectorsOwner);
this._connection.on('close', () => { this._connection.on('close', () => {
this.selectors._removeChannel(selectorsOwner); this.selectors._removeChannel(selectorsOwner);
for (const uid of this._sockets.keys()) this._socksProxyHandler?.cleanup();
this._onSocksClosed(uid);
}); });
(global as any)._playwrightInstance = this; (global as any)._playwrightInstance = this;
} }
@ -93,49 +87,24 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.selectors._addChannel(selectorsOwner); this.selectors._addChannel(selectorsOwner);
} }
// TODO: remove this methods together with PlaywrightClient.
_enablePortForwarding(redirectPortForTest?: number) { _enablePortForwarding(redirectPortForTest?: number) {
this._redirectPortForTest = redirectPortForTest; const socksSupport = this._initializer.socksSupport;
this._channel.on('socksRequested', ({ uid, host, port }) => this._onSocksRequested(uid, host, port)); if (!socksSupport)
this._channel.on('socksData', ({ uid, data }) => this._onSocksData(uid, Buffer.from(data, 'base64'))); return;
this._channel.on('socksClosed', ({ uid }) => this._onSocksClosed(uid)); const handler = new socks.SocksProxyHandler(redirectPortForTest);
} this._socksProxyHandler = handler;
handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => socksSupport.socksConnected(payload).catch(() => {}));
private async _onSocksRequested(uid: string, host: string, port: number): Promise<void> { handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => socksSupport.socksData({ uid: payload.uid, data: payload.data.toString('base64') }).catch(() => {}));
if (host === 'local.playwright') handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => socksSupport.socksError(payload).catch(() => {}));
host = 'localhost'; handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => socksSupport.socksFailed(payload).catch(() => {}));
try { handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => socksSupport.socksEnd(payload).catch(() => {}));
if (this._redirectPortForTest) socksSupport.on('socksRequested', payload => handler.socketRequested(payload));
port = this._redirectPortForTest; socksSupport.on('socksClosed', payload => handler.socketClosed(payload));
const { address } = await dnsLookupAsync(host); socksSupport.on('socksData', payload => handler.sendSocketData({ uid: payload.uid, data: Buffer.from(payload.data, 'base64') }));
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);
} }
static from(channel: channels.PlaywrightChannel): Playwright { static from(channel: channels.PlaywrightChannel): Playwright {
return (channel as any)._object; return (channel as any)._object;
} }
private _onSocksClosed(uid: string): void {
this._sockets.get(uid)?.destroy();
this._sockets.delete(uid);
}
} }

View file

@ -24,6 +24,8 @@ import WebSocket from 'ws';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher'; import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import { getUserAgent, makeWaitForNextTask } from '../utils/utils'; import { getUserAgent, makeWaitForNextTask } from '../utils/utils';
import { ManualPromise } from '../utils/async'; 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 { export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeChannel> implements channels.BrowserTypeChannel {
_type_BrowserType = true; _type_BrowserType = true;
@ -65,11 +67,16 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
headers: paramsHeaders, headers: paramsHeaders,
followRedirects: true, followRedirects: true,
}); });
let socksInterceptor: SocksInterceptor | undefined;
const pipe = new JsonPipeDispatcher(this._scope); const pipe = new JsonPipeDispatcher(this._scope);
const openPromise = new ManualPromise<{ pipe: JsonPipeDispatcher }>(); const openPromise = new ManualPromise<{ pipe: JsonPipeDispatcher }>();
ws.on('open', () => openPromise.resolve({ pipe })); ws.on('open', () => openPromise.resolve({ pipe }));
ws.on('close', () => pipe.wasClosed()); ws.on('close', () => {
socksInterceptor?.cleanup();
pipe.wasClosed();
});
ws.on('error', error => { ws.on('error', error => {
socksInterceptor?.cleanup();
if (openPromise.isDone()) { if (openPromise.isDone()) {
pipe.wasClosed(error); pipe.wasClosed(error);
} else { } else {
@ -77,12 +84,19 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
openPromise.reject(error); openPromise.reject(error);
} }
}); });
pipe.on('close', () => ws.close()); pipe.on('close', () => {
socksInterceptor?.cleanup();
ws.close();
});
pipe.on('message', message => ws.send(JSON.stringify(message))); pipe.on('message', message => ws.send(JSON.stringify(message)));
ws.addEventListener('message', event => { ws.addEventListener('message', event => {
waitForNextTask(() => { waitForNextTask(() => {
try { 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) { } catch (e) {
ws.close(); ws.close();
} }
@ -91,3 +105,55 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
return openPromise; 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;
}
}

View file

@ -18,7 +18,7 @@ import * as channels from '../protocol/channels';
import { Browser } from '../server/browser'; import { Browser } from '../server/browser';
import { GlobalAPIRequestContext } from '../server/fetch'; import { GlobalAPIRequestContext } from '../server/fetch';
import { Playwright } from '../server/playwright'; 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 * as types from '../server/types';
import { AndroidDispatcher } from './androidDispatcher'; import { AndroidDispatcher } from './androidDispatcher';
import { BrowserTypeDispatcher } from './browserTypeDispatcher'; import { BrowserTypeDispatcher } from './browserTypeDispatcher';
@ -28,11 +28,11 @@ import { LocalUtilsDispatcher } from './localUtilsDispatcher';
import { APIRequestContextDispatcher } from './networkDispatchers'; import { APIRequestContextDispatcher } from './networkDispatchers';
import { SelectorsDispatcher } from './selectorsDispatcher'; import { SelectorsDispatcher } from './selectorsDispatcher';
import { ConnectedBrowserDispatcher } from './browserDispatcher'; import { ConnectedBrowserDispatcher } from './browserDispatcher';
import { createGuid } from '../utils/utils';
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightChannel> implements channels.PlaywrightChannel { export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightChannel> implements channels.PlaywrightChannel {
_type_Playwright; _type_Playwright;
private _browserDispatcher: ConnectedBrowserDispatcher | undefined; private _browserDispatcher: ConnectedBrowserDispatcher | undefined;
private _socksProxy: SocksProxy | undefined;
constructor(scope: DispatcherScope, playwright: Playwright, socksProxy?: SocksProxy, preLaunchedBrowser?: Browser) { constructor(scope: DispatcherScope, playwright: Playwright, socksProxy?: SocksProxy, preLaunchedBrowser?: Browser) {
const descriptors = require('../server/deviceDescriptors') as types.Devices; const descriptors = require('../server/deviceDescriptors') as types.Devices;
@ -49,35 +49,10 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
deviceDescriptors, deviceDescriptors,
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors), selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
preLaunchedBrowser: browserDispatcher, preLaunchedBrowser: browserDispatcher,
socksSupport: socksProxy ? new SocksSupportDispatcher(scope, socksProxy) : undefined,
}, false); }, false);
this._type_Playwright = true; this._type_Playwright = true;
this._browserDispatcher = browserDispatcher; 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 socksFailed(params: channels.PlaywrightSocksFailedParams): Promise<void> {
this._socksProxy?.socketFailed(params.uid, params.errorCode);
}
async socksData(params: channels.PlaywrightSocksDataParams): Promise<void> {
this._socksProxy?.sendSocketData(params.uid, Buffer.from(params.data, 'base64'));
}
async socksError(params: channels.PlaywrightSocksErrorParams): Promise<void> {
this._socksProxy?.sendSocketError(params.uid, params.error);
}
async socksEnd(params: channels.PlaywrightSocksEndParams): Promise<void> {
this._socksProxy?.sendSocketEnd(params.uid);
} }
async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise<channels.PlaywrightNewRequestResult> { async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise<channels.PlaywrightNewRequestResult> {
@ -94,3 +69,37 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
await this._browserDispatcher?.cleanupContexts(); await this._browserDispatcher?.cleanupContexts();
} }
} }
class SocksSupportDispatcher extends Dispatcher<{ guid: string }, channels.SocksSupportChannel> implements channels.SocksSupportChannel {
_type_SocksSupport: boolean;
private _socksProxy: SocksProxy;
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 socksConnected(params: channels.SocksSupportSocksConnectedParams): Promise<void> {
this._socksProxy?.socketConnected(params);
}
async socksFailed(params: channels.SocksSupportSocksFailedParams): Promise<void> {
this._socksProxy?.socketFailed(params);
}
async socksData(params: channels.SocksSupportSocksDataParams): Promise<void> {
this._socksProxy?.sendSocketData({ uid: params.uid, data: Buffer.from(params.data, 'base64') });
}
async socksError(params: channels.SocksSupportSocksErrorParams): Promise<void> {
this._socksProxy?.sendSocketError(params);
}
async socksEnd(params: channels.SocksSupportSocksEndParams): Promise<void> {
this._socksProxy?.sendSocketEnd(params);
}
}

View file

@ -20,7 +20,7 @@ import { DispatcherConnection, Root } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { createPlaywright } from '../server/playwright'; import { createPlaywright } from '../server/playwright';
import { gracefullyCloseAll } from '../utils/processLauncher'; import { gracefullyCloseAll } from '../utils/processLauncher';
import { SocksProxy } from '../server/socksProxy'; import { SocksProxy } from '../utils/socksProxy';
function launchGridWorker(gridURL: string, agentId: string, workerId: string) { function launchGridWorker(gridURL: string, agentId: string, workerId: string) {
const log = debug(`pw:grid:worker${workerId}`); const log = debug(`pw:grid:worker${workerId}`);

View file

@ -50,6 +50,7 @@ export type InitializerTraits<T> =
T extends BrowserChannel ? BrowserInitializer : T extends BrowserChannel ? BrowserInitializer :
T extends BrowserTypeChannel ? BrowserTypeInitializer : T extends BrowserTypeChannel ? BrowserTypeInitializer :
T extends SelectorsChannel ? SelectorsInitializer : T extends SelectorsChannel ? SelectorsInitializer :
T extends SocksSupportChannel ? SocksSupportInitializer :
T extends PlaywrightChannel ? PlaywrightInitializer : T extends PlaywrightChannel ? PlaywrightInitializer :
T extends RootChannel ? RootInitializer : T extends RootChannel ? RootInitializer :
T extends LocalUtilsChannel ? LocalUtilsInitializer : T extends LocalUtilsChannel ? LocalUtilsInitializer :
@ -85,6 +86,7 @@ export type EventsTraits<T> =
T extends BrowserChannel ? BrowserEvents : T extends BrowserChannel ? BrowserEvents :
T extends BrowserTypeChannel ? BrowserTypeEvents : T extends BrowserTypeChannel ? BrowserTypeEvents :
T extends SelectorsChannel ? SelectorsEvents : T extends SelectorsChannel ? SelectorsEvents :
T extends SocksSupportChannel ? SocksSupportEvents :
T extends PlaywrightChannel ? PlaywrightEvents : T extends PlaywrightChannel ? PlaywrightEvents :
T extends RootChannel ? RootEvents : T extends RootChannel ? RootEvents :
T extends LocalUtilsChannel ? LocalUtilsEvents : T extends LocalUtilsChannel ? LocalUtilsEvents :
@ -120,6 +122,7 @@ export type EventTargetTraits<T> =
T extends BrowserChannel ? BrowserEventTarget : T extends BrowserChannel ? BrowserEventTarget :
T extends BrowserTypeChannel ? BrowserTypeEventTarget : T extends BrowserTypeChannel ? BrowserTypeEventTarget :
T extends SelectorsChannel ? SelectorsEventTarget : T extends SelectorsChannel ? SelectorsEventTarget :
T extends SocksSupportChannel ? SocksSupportEventTarget :
T extends PlaywrightChannel ? PlaywrightEventTarget : T extends PlaywrightChannel ? PlaywrightEventTarget :
T extends RootChannel ? RootEventTarget : T extends RootChannel ? RootEventTarget :
T extends LocalUtilsChannel ? LocalUtilsEventTarget : T extends LocalUtilsChannel ? LocalUtilsEventTarget :
@ -423,74 +426,15 @@ export type PlaywrightInitializer = {
}[], }[],
selectors: SelectorsChannel, selectors: SelectorsChannel,
preLaunchedBrowser?: BrowserChannel, preLaunchedBrowser?: BrowserChannel,
socksSupport?: SocksSupportChannel,
}; };
export interface PlaywrightEventTarget { export interface PlaywrightEventTarget {
on(event: 'socksRequested', callback: (params: PlaywrightSocksRequestedEvent) => void): this;
on(event: 'socksData', callback: (params: PlaywrightSocksDataEvent) => void): this;
on(event: 'socksClosed', callback: (params: PlaywrightSocksClosedEvent) => void): this;
} }
export interface PlaywrightChannel extends PlaywrightEventTarget, Channel { export interface PlaywrightChannel extends PlaywrightEventTarget, Channel {
_type_Playwright: boolean; _type_Playwright: boolean;
socksConnected(params: PlaywrightSocksConnectedParams, metadata?: Metadata): Promise<PlaywrightSocksConnectedResult>;
socksFailed(params: PlaywrightSocksFailedParams, metadata?: Metadata): Promise<PlaywrightSocksFailedResult>;
socksData(params: PlaywrightSocksDataParams, metadata?: Metadata): Promise<PlaywrightSocksDataResult>;
socksError(params: PlaywrightSocksErrorParams, metadata?: Metadata): Promise<PlaywrightSocksErrorResult>;
socksEnd(params: PlaywrightSocksEndParams, metadata?: Metadata): Promise<PlaywrightSocksEndResult>;
newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>; newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>;
hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise<PlaywrightHideHighlightResult>; hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise<PlaywrightHideHighlightResult>;
} }
export type PlaywrightSocksRequestedEvent = {
uid: string,
host: string,
port: number,
};
export type PlaywrightSocksDataEvent = {
uid: string,
data: Binary,
};
export type PlaywrightSocksClosedEvent = {
uid: string,
};
export type PlaywrightSocksConnectedParams = {
uid: string,
host: string,
port: number,
};
export type PlaywrightSocksConnectedOptions = {
};
export type PlaywrightSocksConnectedResult = void;
export type PlaywrightSocksFailedParams = {
uid: string,
errorCode: string,
};
export type PlaywrightSocksFailedOptions = {
};
export type PlaywrightSocksFailedResult = void;
export type PlaywrightSocksDataParams = {
uid: string,
data: Binary,
};
export type PlaywrightSocksDataOptions = {
};
export type PlaywrightSocksDataResult = void;
export type PlaywrightSocksErrorParams = {
uid: string,
error: string,
};
export type PlaywrightSocksErrorOptions = {
};
export type PlaywrightSocksErrorResult = void;
export type PlaywrightSocksEndParams = {
uid: string,
};
export type PlaywrightSocksEndOptions = {
};
export type PlaywrightSocksEndResult = void;
export type PlaywrightNewRequestParams = { export type PlaywrightNewRequestParams = {
baseURL?: string, baseURL?: string,
userAgent?: string, userAgent?: string,
@ -543,9 +487,80 @@ export type PlaywrightHideHighlightOptions = {};
export type PlaywrightHideHighlightResult = void; export type PlaywrightHideHighlightResult = void;
export interface PlaywrightEvents { export interface PlaywrightEvents {
'socksRequested': PlaywrightSocksRequestedEvent; }
'socksData': PlaywrightSocksDataEvent;
'socksClosed': PlaywrightSocksClosedEvent; // ----------- SocksSupport -----------
export type SocksSupportInitializer = {};
export interface SocksSupportEventTarget {
on(event: 'socksRequested', callback: (params: SocksSupportSocksRequestedEvent) => void): this;
on(event: 'socksData', callback: (params: SocksSupportSocksDataEvent) => void): this;
on(event: 'socksClosed', callback: (params: SocksSupportSocksClosedEvent) => void): this;
}
export interface SocksSupportChannel extends SocksSupportEventTarget, Channel {
_type_SocksSupport: boolean;
socksConnected(params: SocksSupportSocksConnectedParams, metadata?: Metadata): Promise<SocksSupportSocksConnectedResult>;
socksFailed(params: SocksSupportSocksFailedParams, metadata?: Metadata): Promise<SocksSupportSocksFailedResult>;
socksData(params: SocksSupportSocksDataParams, metadata?: Metadata): Promise<SocksSupportSocksDataResult>;
socksError(params: SocksSupportSocksErrorParams, metadata?: Metadata): Promise<SocksSupportSocksErrorResult>;
socksEnd(params: SocksSupportSocksEndParams, metadata?: Metadata): Promise<SocksSupportSocksEndResult>;
}
export type SocksSupportSocksRequestedEvent = {
uid: string,
host: string,
port: number,
};
export type SocksSupportSocksDataEvent = {
uid: string,
data: Binary,
};
export type SocksSupportSocksClosedEvent = {
uid: string,
};
export type SocksSupportSocksConnectedParams = {
uid: string,
host: string,
port: number,
};
export type SocksSupportSocksConnectedOptions = {
};
export type SocksSupportSocksConnectedResult = void;
export type SocksSupportSocksFailedParams = {
uid: string,
errorCode: string,
};
export type SocksSupportSocksFailedOptions = {
};
export type SocksSupportSocksFailedResult = void;
export type SocksSupportSocksDataParams = {
uid: string,
data: Binary,
};
export type SocksSupportSocksDataOptions = {
};
export type SocksSupportSocksDataResult = void;
export type SocksSupportSocksErrorParams = {
uid: string,
error: string,
};
export type SocksSupportSocksErrorOptions = {
};
export type SocksSupportSocksErrorResult = void;
export type SocksSupportSocksEndParams = {
uid: string,
};
export type SocksSupportSocksEndOptions = {
};
export type SocksSupportSocksEndResult = void;
export interface SocksSupportEvents {
'socksRequested': SocksSupportSocksRequestedEvent;
'socksData': SocksSupportSocksDataEvent;
'socksClosed': SocksSupportSocksClosedEvent;
} }
// ----------- Selectors ----------- // ----------- Selectors -----------
@ -588,11 +603,15 @@ export type BrowserTypeConnectParams = {
headers?: any, headers?: any,
slowMo?: number, slowMo?: number,
timeout?: number, timeout?: number,
enableSocksProxy?: boolean,
socksProxyRedirectPortForTest?: number,
}; };
export type BrowserTypeConnectOptions = { export type BrowserTypeConnectOptions = {
headers?: any, headers?: any,
slowMo?: number, slowMo?: number,
timeout?: number, timeout?: number,
enableSocksProxy?: boolean,
socksProxyRedirectPortForTest?: number,
}; };
export type BrowserTypeConnectResult = { export type BrowserTypeConnectResult = {
pipe: JsonPipeChannel, pipe: JsonPipeChannel,

View file

@ -483,34 +483,10 @@ Playwright:
selectors: Selectors selectors: Selectors
# Only present when connecting remotely via BrowserType.connect() method. # Only present when connecting remotely via BrowserType.connect() method.
preLaunchedBrowser: Browser? preLaunchedBrowser: Browser?
# Only present when socks proxy is supported.
socksSupport: SocksSupport?
commands: commands:
socksConnected:
parameters:
uid: string
host: string
port: number
socksFailed:
parameters:
uid: string
errorCode: string
socksData:
parameters:
uid: string
data: binary
socksError:
parameters:
uid: string
error: string
socksEnd:
parameters:
uid: string
newRequest: newRequest:
parameters: parameters:
baseURL: string? baseURL: string?
@ -548,6 +524,35 @@ Playwright:
hideHighlight: hideHighlight:
SocksSupport:
type: interface
commands:
socksConnected:
parameters:
uid: string
host: string
port: number
socksFailed:
parameters:
uid: string
errorCode: string
socksData:
parameters:
uid: string
data: binary
socksError:
parameters:
uid: string
error: string
socksEnd:
parameters:
uid: string
events: events:
socksRequested: socksRequested:
parameters: parameters:
@ -564,7 +569,6 @@ Playwright:
parameters: parameters:
uid: string uid: string
Selectors: Selectors:
type: interface type: interface
@ -592,6 +596,8 @@ BrowserType:
headers: json? headers: json?
slowMo: number? slowMo: number?
timeout: number? timeout: number?
enableSocksProxy: boolean?
socksProxyRedirectPortForTest: number?
returns: returns:
pipe: JsonPipe pipe: JsonPipe

View file

@ -196,26 +196,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.RootInitializeParams = tObject({ scheme.RootInitializeParams = tObject({
sdkLanguage: tString, sdkLanguage: tString,
}); });
scheme.PlaywrightSocksConnectedParams = tObject({
uid: tString,
host: tString,
port: tNumber,
});
scheme.PlaywrightSocksFailedParams = tObject({
uid: tString,
errorCode: tString,
});
scheme.PlaywrightSocksDataParams = tObject({
uid: tString,
data: tBinary,
});
scheme.PlaywrightSocksErrorParams = tObject({
uid: tString,
error: tString,
});
scheme.PlaywrightSocksEndParams = tObject({
uid: tString,
});
scheme.PlaywrightNewRequestParams = tObject({ scheme.PlaywrightNewRequestParams = tObject({
baseURL: tOptional(tString), baseURL: tOptional(tString),
userAgent: tOptional(tString), userAgent: tOptional(tString),
@ -239,6 +219,26 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
tracesDir: tOptional(tString), tracesDir: tOptional(tString),
}); });
scheme.PlaywrightHideHighlightParams = tOptional(tObject({})); scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
scheme.SocksSupportSocksConnectedParams = tObject({
uid: tString,
host: tString,
port: tNumber,
});
scheme.SocksSupportSocksFailedParams = tObject({
uid: tString,
errorCode: tString,
});
scheme.SocksSupportSocksDataParams = tObject({
uid: tString,
data: tBinary,
});
scheme.SocksSupportSocksErrorParams = tObject({
uid: tString,
error: tString,
});
scheme.SocksSupportSocksEndParams = tObject({
uid: tString,
});
scheme.SelectorsRegisterParams = tObject({ scheme.SelectorsRegisterParams = tObject({
name: tString, name: tString,
source: tString, source: tString,
@ -249,6 +249,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
headers: tOptional(tAny), headers: tOptional(tAny),
slowMo: tOptional(tNumber), slowMo: tOptional(tNumber),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
enableSocksProxy: tOptional(tBoolean),
socksProxyRedirectPortForTest: tOptional(tNumber),
}); });
scheme.BrowserTypeLaunchParams = tObject({ scheme.BrowserTypeLaunchParams = tObject({
channel: tOptional(tString), channel: tOptional(tString),

View file

@ -24,7 +24,7 @@ import { Browser } from '../server/browser';
import { gracefullyCloseAll } from '../utils/processLauncher'; import { gracefullyCloseAll } from '../utils/processLauncher';
import { registry } from '../utils/registry'; import { registry } from '../utils/registry';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { SocksProxy } from '../server/socksProxy'; import { SocksProxy } from '../utils/socksProxy';
const debugLog = debug('pw:server'); const debugLog = debug('pw:server');

View file

@ -1,87 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import net, { AddressInfo } from 'net';
import { debugLogger } from '../utils/debugLogger';
import { SocksConnection, SocksConnectionClient } from '../utils/socksProxy';
import { createGuid } from '../utils/utils';
import EventEmitter from 'events';
export class SocksProxy extends EventEmitter implements SocksConnectionClient {
static Events = {
SocksRequested: 'socksRequested',
SocksData: 'socksData',
SocksClosed: 'socksClosed',
};
private _server: net.Server;
private _connections = new Map<string, SocksConnection>();
constructor() {
super();
this._server = new net.Server((socket: net.Socket) => {
const uid = createGuid();
const connection = new SocksConnection(uid, socket, this);
this._connections.set(uid, connection);
});
}
async listen(port: number): Promise<number> {
return new Promise(f => {
this._server.listen(port, () => {
const port = (this._server.address() as AddressInfo).port;
debugLogger.log('proxy', `Starting socks proxy server on port ${port}`);
f(port);
});
});
}
async close() {
await new Promise(f => this._server.close(f));
}
onSocketRequested(uid: string, host: string, port: number): void {
this.emit(SocksProxy.Events.SocksRequested, { uid, host, port });
}
onSocketData(uid: string, data: Buffer): void {
this.emit(SocksProxy.Events.SocksData, { uid, data: data.toString('base64') });
}
onSocketClosed(uid: string): void {
this.emit(SocksProxy.Events.SocksClosed, { uid });
}
socketConnected(uid: string, host: string, port: number) {
this._connections.get(uid)?.socketConnected(host, port);
}
socketFailed(uid: string, errorCode: string) {
this._connections.get(uid)?.socketFailed(errorCode);
}
sendSocketData(uid: string, buffer: Buffer) {
this._connections.get(uid)?.sendData(buffer);
}
sendSocketEnd(uid: string) {
this._connections.get(uid)?.end();
}
sendSocketError(uid: string, error: string) {
this._connections.get(uid)?.error(error);
}
}

View file

@ -14,8 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import net from 'net'; import dns from 'dns';
import { assert } from './utils'; import EventEmitter from 'events';
import net, { AddressInfo } from 'net';
import util from 'util';
import { debugLogger } from './debugLogger';
import { createSocket } from './netUtils';
import { assert, createGuid } from './utils';
const dnsLookupAsync = util.promisify(dns.lookup);
// https://tools.ietf.org/html/rfc1928 // https://tools.ietf.org/html/rfc1928
@ -50,13 +57,21 @@ enum SocksReply {
AddressTypeNotSupported = 0x08 AddressTypeNotSupported = 0x08
} }
export interface SocksConnectionClient { export type SocksSocketRequestedPayload = { uid: string, host: string, port: number };
onSocketRequested(uid: string, host: string, port: number): void; export type SocksSocketConnectedPayload = { uid: string, host: string, port: number };
onSocketData(uid: string, data: Buffer): void; export type SocksSocketDataPayload = { uid: string, data: Buffer };
onSocketClosed(uid: string): void; export type SocksSocketErrorPayload = { uid: string, error: string };
export type SocksSocketFailedPayload = { uid: string, errorCode: string };
export type SocksSocketClosedPayload = { uid: string };
export type SocksSocketEndPayload = { uid: string };
interface SocksConnectionClient {
onSocketRequested(payload: SocksSocketRequestedPayload): void;
onSocketData(payload: SocksSocketDataPayload): void;
onSocketClosed(payload: SocksSocketClosedPayload): void;
} }
export class SocksConnection { class SocksConnection {
private _buffer = Buffer.from([]); private _buffer = Buffer.from([]);
private _offset = 0; private _offset = 0;
private _fence = 0; private _fence = 0;
@ -94,7 +109,7 @@ export class SocksConnection {
} }
this._socket.off('data', this._boundOnData); this._socket.off('data', this._boundOnData);
this._client.onSocketRequested(this._uid, host, port); this._client.onSocketRequested({ uid: this._uid, host, port });
} }
async _authenticate(): Promise<boolean> { async _authenticate(): Promise<boolean> {
@ -199,7 +214,7 @@ export class SocksConnection {
} }
private _onClose() { private _onClose() {
this._client.onSocketClosed(this._uid); this._client.onSocketClosed({ uid: this._uid });
} }
private _onData(buffer: Buffer) { private _onData(buffer: Buffer) {
@ -220,7 +235,7 @@ export class SocksConnection {
...parseIP(host), // Address ...parseIP(host), // Address
port << 8, port & 0xFF // Port port << 8, port & 0xFF // Port
])); ]));
this._socket.on('data', data => this._client.onSocketData(this._uid, data)); this._socket.on('data', data => this._client.onSocketData({ uid: this._uid, data }));
} }
socketFailed(errorCode: string) { socketFailed(errorCode: string) {
@ -268,3 +283,134 @@ function parseIP(address: string): number[] {
throw new Error('IPv6 is not supported'); throw new Error('IPv6 is not supported');
return address.split('.', 4).map(t => +t); return address.split('.', 4).map(t => +t);
} }
export class SocksProxy extends EventEmitter implements SocksConnectionClient {
static Events = {
SocksRequested: 'socksRequested',
SocksData: 'socksData',
SocksClosed: 'socksClosed',
};
private _server: net.Server;
private _connections = new Map<string, SocksConnection>();
constructor() {
super();
this._server = new net.Server((socket: net.Socket) => {
const uid = createGuid();
const connection = new SocksConnection(uid, socket, this);
this._connections.set(uid, connection);
});
}
async listen(port: number): Promise<number> {
return new Promise(f => {
this._server.listen(port, () => {
const port = (this._server.address() as AddressInfo).port;
debugLogger.log('proxy', `Starting socks proxy server on port ${port}`);
f(port);
});
});
}
async close() {
await new Promise(f => this._server.close(f));
}
onSocketRequested(payload: SocksSocketRequestedPayload) {
this.emit(SocksProxy.Events.SocksRequested, payload);
}
onSocketData(payload: SocksSocketDataPayload): void {
this.emit(SocksProxy.Events.SocksData, payload);
}
onSocketClosed(payload: SocksSocketClosedPayload): void {
this.emit(SocksProxy.Events.SocksClosed, payload);
}
socketConnected({ uid, host, port }: SocksSocketConnectedPayload) {
this._connections.get(uid)?.socketConnected(host, port);
}
socketFailed({ uid, errorCode }: SocksSocketFailedPayload) {
this._connections.get(uid)?.socketFailed(errorCode);
}
sendSocketData({ uid, data }: SocksSocketDataPayload) {
this._connections.get(uid)?.sendData(data);
}
sendSocketEnd({ uid }: SocksSocketEndPayload) {
this._connections.get(uid)?.end();
}
sendSocketError({ uid, error }: SocksSocketErrorPayload) {
this._connections.get(uid)?.error(error);
}
}
export class SocksProxyHandler extends EventEmitter {
static Events = {
SocksConnected: 'socksConnected',
SocksData: 'socksData',
SocksError: 'socksError',
SocksFailed: 'socksFailed',
SocksEnd: 'socksEnd',
};
private _sockets = new Map<string, net.Socket>();
private _redirectPortForTest: number | undefined;
constructor(redirectPortForTest?: number) {
super();
this._redirectPortForTest = redirectPortForTest;
}
cleanup() {
for (const uid of this._sockets.keys())
this.socketClosed({ uid });
}
async socketRequested({ uid, host, port }: SocksSocketRequestedPayload): 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 => {
const payload: SocksSocketDataPayload = { uid, data };
this.emit(SocksProxyHandler.Events.SocksData, payload);
});
socket.on('error', error => {
const payload: SocksSocketErrorPayload = { uid, error: error.message };
this.emit(SocksProxyHandler.Events.SocksError, payload);
this._sockets.delete(uid);
});
socket.on('end', () => {
const payload: SocksSocketEndPayload = { uid };
this.emit(SocksProxyHandler.Events.SocksEnd, payload);
this._sockets.delete(uid);
});
const localAddress = socket.localAddress;
const localPort = socket.localPort;
this._sockets.set(uid, socket);
const payload: SocksSocketConnectedPayload = { uid, host: localAddress, port: localPort };
this.emit(SocksProxyHandler.Events.SocksConnected, payload);
} catch (error) {
const payload: SocksSocketFailedPayload = { uid, errorCode: error.code };
this.emit(SocksProxyHandler.Events.SocksFailed, payload);
}
}
sendSocketData({ uid, data }: SocksSocketDataPayload): void {
this._sockets.get(uid)?.write(data);
}
socketClosed({ uid }: SocksSocketClosedPayload): void {
this._sockets.get(uid)?.destroy();
this._sockets.delete(uid);
}
}

View file

@ -26,6 +26,8 @@ it.use({
} }
}); });
it.skip(({ mode }) => mode === 'service');
it('should scope context handles', async ({ browserType, server }) => { it('should scope context handles', async ({ browserType, server }) => {
const browser = await browserType.launch(); const browser = await browserType.launch();
const GOLDEN_PRECONDITION = { const GOLDEN_PRECONDITION = {