diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index d0635f3ecf..d666d39fd2 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -128,7 +128,12 @@ export class BrowserType extends ChannelOwner 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 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; diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 612053b837..26b16e265f 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -55,6 +55,9 @@ class Root extends ChannelOwner { } } +class DummyChannelOwner extends ChannelOwner { +} + export class Connection extends EventEmitter { readonly _objects = new Map(); 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; diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index e6345adb7a..7064fe561d 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -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 { readonly request: APIRequest; readonly errors: { TimeoutError: typeof TimeoutError }; _utils: LocalUtils; - private _sockets = new Map(); - 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 { 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 { 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 { - 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); - } } diff --git a/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts index 22d459e49b..c79d903a43 100644 --- a/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/dispatchers/browserTypeDispatcher.ts @@ -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 implements channels.BrowserTypeChannel { _type_BrowserType = true; @@ -65,11 +67,16 @@ export class BrowserTypeDispatcher extends Dispatcher(); 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 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(); + + 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; + } +} diff --git a/packages/playwright-core/src/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/dispatchers/playwrightDispatcher.ts index 775b09795f..240f7055c7 100644 --- a/packages/playwright-core/src/dispatchers/playwrightDispatcher.ts +++ b/packages/playwright-core/src/dispatchers/playwrightDispatcher.ts @@ -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 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 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 { - this._socksProxy?.socketConnected(params.uid, params.host, params.port); - } - - async socksFailed(params: channels.PlaywrightSocksFailedParams): Promise { - this._socksProxy?.socketFailed(params.uid, params.errorCode); - } - - async socksData(params: channels.PlaywrightSocksDataParams): Promise { - this._socksProxy?.sendSocketData(params.uid, Buffer.from(params.data, 'base64')); - } - - async socksError(params: channels.PlaywrightSocksErrorParams): Promise { - this._socksProxy?.sendSocketError(params.uid, params.error); - } - - async socksEnd(params: channels.PlaywrightSocksEndParams): Promise { - this._socksProxy?.sendSocketEnd(params.uid); } async newRequest(params: channels.PlaywrightNewRequestParams, metadata?: channels.Metadata): Promise { @@ -94,3 +69,37 @@ export class PlaywrightDispatcher extends Dispatcher 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 { + this._socksProxy?.socketConnected(params); + } + + async socksFailed(params: channels.SocksSupportSocksFailedParams): Promise { + this._socksProxy?.socketFailed(params); + } + + async socksData(params: channels.SocksSupportSocksDataParams): Promise { + this._socksProxy?.sendSocketData({ uid: params.uid, data: Buffer.from(params.data, 'base64') }); + } + + async socksError(params: channels.SocksSupportSocksErrorParams): Promise { + this._socksProxy?.sendSocketError(params); + } + + async socksEnd(params: channels.SocksSupportSocksEndParams): Promise { + this._socksProxy?.sendSocketEnd(params); + } +} diff --git a/packages/playwright-core/src/grid/gridWorker.ts b/packages/playwright-core/src/grid/gridWorker.ts index 1c8e9d356f..dddf042a08 100644 --- a/packages/playwright-core/src/grid/gridWorker.ts +++ b/packages/playwright-core/src/grid/gridWorker.ts @@ -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}`); diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index fee13995ec..a42c5d58bc 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -50,6 +50,7 @@ export type InitializerTraits = 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 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 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; - socksFailed(params: PlaywrightSocksFailedParams, metadata?: Metadata): Promise; - socksData(params: PlaywrightSocksDataParams, metadata?: Metadata): Promise; - socksError(params: PlaywrightSocksErrorParams, metadata?: Metadata): Promise; - socksEnd(params: PlaywrightSocksEndParams, metadata?: Metadata): Promise; newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise; hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise; } -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; + socksFailed(params: SocksSupportSocksFailedParams, metadata?: Metadata): Promise; + socksData(params: SocksSupportSocksDataParams, metadata?: Metadata): Promise; + socksError(params: SocksSupportSocksErrorParams, metadata?: Metadata): Promise; + socksEnd(params: SocksSupportSocksEndParams, metadata?: Metadata): Promise; +} +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, diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index 16cc70bc96..481720d6dd 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -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 diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 645883f2bd..2714d17988 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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), diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index 05f9df709a..e773602d94 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -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'); diff --git a/packages/playwright-core/src/server/socksProxy.ts b/packages/playwright-core/src/server/socksProxy.ts deleted file mode 100644 index 58313eac75..0000000000 --- a/packages/playwright-core/src/server/socksProxy.ts +++ /dev/null @@ -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(); - - 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 { - 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); - } -} diff --git a/packages/playwright-core/src/utils/socksProxy.ts b/packages/playwright-core/src/utils/socksProxy.ts index 24fdca4312..b4c2fb65f4 100644 --- a/packages/playwright-core/src/utils/socksProxy.ts +++ b/packages/playwright-core/src/utils/socksProxy.ts @@ -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 { @@ -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(); + + 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 { + 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(); + 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 { + 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); + } +} diff --git a/tests/channels.spec.ts b/tests/channels.spec.ts index 1ead59a433..d4e444b1ce 100644 --- a/tests/channels.spec.ts +++ b/tests/channels.spec.ts @@ -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 = {