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 () => {
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();
@ -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;

View File

@ -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 => {};
@ -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;

View File

@ -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';
@ -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,
@ -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);
@ -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;
}
@ -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);
}
}

View File

@ -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;
@ -65,11 +67,16 @@ 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 {
@ -77,12 +84,19 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
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();
}
@ -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;
}
}

View File

@ -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';
@ -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;
@ -49,35 +49,10 @@ 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 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> {
@ -94,3 +69,37 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
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 { 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}`);

View File

@ -50,6 +50,7 @@ export type InitializerTraits<T> =
T extends BrowserChannel ? BrowserInitializer :
T extends BrowserTypeChannel ? BrowserTypeInitializer :
T extends SelectorsChannel ? SelectorsInitializer :
T extends SocksSupportChannel ? SocksSupportInitializer :
T extends PlaywrightChannel ? PlaywrightInitializer :
T extends RootChannel ? RootInitializer :
T extends LocalUtilsChannel ? LocalUtilsInitializer :
@ -85,6 +86,7 @@ export type EventsTraits<T> =
T extends BrowserChannel ? BrowserEvents :
T extends BrowserTypeChannel ? BrowserTypeEvents :
T extends SelectorsChannel ? SelectorsEvents :
T extends SocksSupportChannel ? SocksSupportEvents :
T extends PlaywrightChannel ? PlaywrightEvents :
T extends RootChannel ? RootEvents :
T extends LocalUtilsChannel ? LocalUtilsEvents :
@ -120,6 +122,7 @@ export type EventTargetTraits<T> =
T extends BrowserChannel ? BrowserEventTarget :
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
T extends SelectorsChannel ? SelectorsEventTarget :
T extends SocksSupportChannel ? SocksSupportEventTarget :
T extends PlaywrightChannel ? PlaywrightEventTarget :
T extends RootChannel ? RootEventTarget :
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
@ -423,74 +426,15 @@ export type PlaywrightInitializer = {
}[],
selectors: SelectorsChannel,
preLaunchedBrowser?: BrowserChannel,
socksSupport?: SocksSupportChannel,
};
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 {
_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>;
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 = {
baseURL?: string,
userAgent?: string,
@ -543,9 +487,80 @@ export type PlaywrightHideHighlightOptions = {};
export type PlaywrightHideHighlightResult = void;
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 -----------
@ -588,11 +603,15 @@ export type BrowserTypeConnectParams = {
headers?: any,
slowMo?: number,
timeout?: number,
enableSocksProxy?: boolean,
socksProxyRedirectPortForTest?: number,
};
export type BrowserTypeConnectOptions = {
headers?: any,
slowMo?: number,
timeout?: number,
enableSocksProxy?: boolean,
socksProxyRedirectPortForTest?: number,
};
export type BrowserTypeConnectResult = {
pipe: JsonPipeChannel,

View File

@ -483,34 +483,10 @@ Playwright:
selectors: Selectors
# Only present when connecting remotely via BrowserType.connect() method.
preLaunchedBrowser: Browser?
# Only present when socks proxy is supported.
socksSupport: SocksSupport?
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:
parameters:
baseURL: string?
@ -548,6 +524,35 @@ Playwright:
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:
socksRequested:
parameters:
@ -564,7 +569,6 @@ Playwright:
parameters:
uid: string
Selectors:
type: interface
@ -592,6 +596,8 @@ BrowserType:
headers: json?
slowMo: number?
timeout: number?
enableSocksProxy: boolean?
socksProxyRedirectPortForTest: number?
returns:
pipe: JsonPipe

View File

@ -196,26 +196,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.RootInitializeParams = tObject({
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({
baseURL: tOptional(tString),
userAgent: tOptional(tString),
@ -239,6 +219,26 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
tracesDir: tOptional(tString),
});
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({
name: tString,
source: tString,
@ -249,6 +249,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
headers: tOptional(tAny),
slowMo: tOptional(tNumber),
timeout: tOptional(tNumber),
enableSocksProxy: tOptional(tBoolean),
socksProxyRedirectPortForTest: tOptional(tNumber),
});
scheme.BrowserTypeLaunchParams = tObject({
channel: tOptional(tString),

View File

@ -24,7 +24,7 @@ import { Browser } from '../server/browser';
import { gracefullyCloseAll } from '../utils/processLauncher';
import { registry } from '../utils/registry';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { SocksProxy } from '../server/socksProxy';
import { SocksProxy } from '../utils/socksProxy';
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.
*/
import net from 'net';
import { assert } from './utils';
import dns from 'dns';
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
@ -50,13 +57,21 @@ enum SocksReply {
AddressTypeNotSupported = 0x08
}
export interface SocksConnectionClient {
onSocketRequested(uid: string, host: string, port: number): void;
onSocketData(uid: string, data: Buffer): void;
onSocketClosed(uid: string): void;
export type SocksSocketRequestedPayload = { uid: string, host: string, port: number };
export type SocksSocketConnectedPayload = { uid: string, host: string, port: number };
export type SocksSocketDataPayload = { uid: string, data: Buffer };
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 _offset = 0;
private _fence = 0;
@ -94,7 +109,7 @@ export class SocksConnection {
}
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> {
@ -199,7 +214,7 @@ export class SocksConnection {
}
private _onClose() {
this._client.onSocketClosed(this._uid);
this._client.onSocketClosed({ uid: this._uid });
}
private _onData(buffer: Buffer) {
@ -220,7 +235,7 @@ export class SocksConnection {
...parseIP(host), // Address
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) {
@ -268,3 +283,134 @@ function parseIP(address: string): number[] {
throw new Error('IPv6 is not supported');
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 }) => {
const browser = await browserType.launch();
const GOLDEN_PRECONDITION = {