diff --git a/packages/playwright-core/src/androidServerImpl.ts b/packages/playwright-core/src/androidServerImpl.ts index dfbb1d43e5..a5c581d044 100644 --- a/packages/playwright-core/src/androidServerImpl.ts +++ b/packages/playwright-core/src/androidServerImpl.ts @@ -49,7 +49,7 @@ export class AndroidServerLauncherImpl { const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`; // 2. Start the server - const server = new PlaywrightServer({ path, maxConnections: 1, preLaunchedAndroidDevice: device, browserProxyMode: 'disabled' }); + const server = new PlaywrightServer({ path, maxConnections: 1, preLaunchedAndroidDevice: device, browserProxyMode: 'client' }); const wsEndpoint = await server.listen(options.port); // 3. Return the BrowserServer interface diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index 9e39a15385..d32c4c589e 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -26,6 +26,7 @@ import { createPlaywright } from './server/playwright'; import { PlaywrightServer } from './remote/playwrightServer'; import { helper } from './server/helper'; import { rewriteErrorMessage } from './utils/stackTrace'; +import { SocksProxy } from './common/socksProxy'; export class BrowserServerLauncherImpl implements BrowserServerLauncher { private _browserName: 'chromium' | 'firefox' | 'webkit'; @@ -36,6 +37,9 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { async launchServer(options: LaunchServerOptions = {}): Promise { const playwright = createPlaywright('javascript'); + const socksProxy = new SocksProxy(); + playwright.options.socksProxyPort = await socksProxy.listen(0); + // 1. Pre-launch the browser const metadata = serverSideCallMetadata(); const browser = await playwright[this._browserName].launch(metadata, { @@ -52,7 +56,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`; // 2. Start the server - const server = new PlaywrightServer({ path, maxConnections: Infinity, browserProxyMode: 'disabled', preLaunchedBrowser: browser }); + const server = new PlaywrightServer({ path, maxConnections: Infinity, browserProxyMode: 'client', preLaunchedBrowser: browser, preLaunchedSocksProxy: socksProxy }); const wsEndpoint = await server.listen(options.port); // 3. Return the BrowserServer interface @@ -63,7 +67,8 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { browserServer.kill = () => browser.options.browserProcess.kill(); (browserServer as any)._disconnectForTest = () => server.close(); (browserServer as any)._userDataDirForTest = (browser as any)._userDataDirForTest; - browser.options.browserProcess.onclose = async (exitCode, signal) => { + browser.options.browserProcess.onclose = (exitCode, signal) => { + socksProxy.close().catch(() => {}); server.close(); browserServer.emit('close', exitCode, signal); }; diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index 600eac9c37..39a5238c11 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -270,7 +270,7 @@ program .option('--port ', 'Server port') .option('--path ', 'Endpoint Path', '/') .option('--max-clients ', 'Maximum clients') - .option('--proxy-mode ', 'Either `client`, `tether` or `disabled`. Defaults to `client`.', 'client') + .option('--proxy-mode ', 'Either `client` or `tether`. Defaults to `client`.', 'client') .action(function(options) { runServer({ port: options.port ? +options.port : undefined, diff --git a/packages/playwright-core/src/cli/driver.ts b/packages/playwright-core/src/cli/driver.ts index c44a57c537..d5b976c625 100644 --- a/packages/playwright-core/src/cli/driver.ts +++ b/packages/playwright-core/src/cli/driver.ts @@ -50,7 +50,7 @@ export type RunServerOptions = { port?: number, path?: string, maxConnections?: number, - browserProxyMode?: 'client' | 'tether' | 'disabled', + browserProxyMode?: 'client' | 'tether', ownedByTetherClient?: boolean, }; diff --git a/packages/playwright-core/src/common/socksProxy.ts b/packages/playwright-core/src/common/socksProxy.ts index 048a19bc89..9a8e0c271c 100644 --- a/packages/playwright-core/src/common/socksProxy.ts +++ b/packages/playwright-core/src/common/socksProxy.ts @@ -317,7 +317,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { }); } - setProxyPattern(pattern: string | undefined) { + setPattern(pattern: string | undefined) { this._pattern = pattern; } @@ -364,6 +364,8 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { } async close() { + if (this._closed) + return; this._closed = true; for (const socket of this._sockets) socket.destroy(); diff --git a/packages/playwright-core/src/grid/gridBrowserWorker.ts b/packages/playwright-core/src/grid/gridBrowserWorker.ts index c424d216e7..5aa175100e 100644 --- a/packages/playwright-core/src/grid/gridBrowserWorker.ts +++ b/packages/playwright-core/src/grid/gridBrowserWorker.ts @@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str const log = debug(`pw:grid:worker:${workerId}`); log('created'); const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`); - new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { socksProxy: '*', browserName, launchOptions: {} }, { }, log, async () => { + new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { socksProxyPattern: '*', browserName, launchOptions: {} }, { }, log, async () => { log('exiting process'); setTimeout(() => process.exit(0), 30000); // Meanwhile, try to gracefully close all browsers. diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts index 202c644e32..005918f686 100644 --- a/packages/playwright-core/src/remote/playwrightConnection.ts +++ b/packages/playwright-core/src/remote/playwrightConnection.ts @@ -25,10 +25,10 @@ import type { LaunchOptions } from '../server/types'; import { AndroidDevice } from '../server/android/android'; import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher'; -export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser' | 'network-tethering'; +export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android' | 'network-tethering'; type Options = { - socksProxy: string | undefined, + socksProxyPattern: string | undefined, browserName: string | null, launchOptions: LaunchOptions, }; @@ -37,7 +37,8 @@ type PreLaunched = { playwright?: Playwright | undefined; browser?: Browser | undefined; androidDevice?: AndroidDevice | undefined; - networkTetheringSocksProxy?: SocksProxy | undefined; + ownedSocksProxy?: SocksProxy | undefined; + sharedSocksProxy?: SocksProxy | undefined; }; export class PlaywrightConnection { @@ -55,9 +56,9 @@ export class PlaywrightConnection { this._ws = ws; this._preLaunched = preLaunched; this._options = options; - if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser') + if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android') assert(preLaunched.playwright); - if (clientType === 'pre-launched-browser') + if (clientType === 'pre-launched-browser-or-android') assert(preLaunched.browser || preLaunched.androidDevice); this._onClose = onClose; this._debugLog = log; @@ -84,7 +85,7 @@ export class PlaywrightConnection { this._root = new RootDispatcher(this._dispatcherConnection, async scope => { if (clientType === 'reuse-browser') return await this._initReuseBrowsersMode(scope); - if (clientType === 'pre-launched-browser') + if (clientType === 'pre-launched-browser-or-android') return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope); if (clientType === 'launch-browser') return await this._initLaunchBrowserMode(scope); @@ -99,8 +100,9 @@ export class PlaywrightConnection { private async _initPlaywrightTetheringMode(scope: RootDispatcher) { this._debugLog(`engaged playwright.tethering mode`); const playwright = createPlaywright('javascript'); - this._preLaunched.networkTetheringSocksProxy?.setProxyPattern(this._options.socksProxy); - return new PlaywrightDispatcher(scope, playwright, this._preLaunched.networkTetheringSocksProxy); + this._preLaunched.sharedSocksProxy?.setPattern(this._options.socksProxyPattern); + // Tethering client owns the shared socks proxy. + return new PlaywrightDispatcher(scope, playwright, this._preLaunched.sharedSocksProxy); } private async _initPlaywrightConnectMode(scope: RootDispatcher) { @@ -111,15 +113,30 @@ export class PlaywrightConnection { await Promise.all(playwright.allBrowsers().map(browser => browser.close())); }); - const socksProxy = await this._configureSocksProxy(playwright); - return new PlaywrightDispatcher(scope, playwright, socksProxy); + let ownedSocksProxy: SocksProxy | undefined; + if (this._preLaunched.sharedSocksProxy) { + // Note: tethering client configures the pattern, and connected client's pattern is ignored. + playwright.options.socksProxyPort = this._preLaunched.sharedSocksProxy.port(); + this._debugLog(`using shared socks proxy on port ${playwright.options.socksProxyPort}`); + } else { + ownedSocksProxy = await this._createOwnedSocksProxy(playwright); + } + return new PlaywrightDispatcher(scope, playwright, ownedSocksProxy); } private async _initLaunchBrowserMode(scope: RootDispatcher) { this._debugLog(`engaged launch mode for "${this._options.browserName}"`); - const playwright = createPlaywright('javascript'); - const socksProxy = await this._configureSocksProxy(playwright); + + let ownedSocksProxy: SocksProxy | undefined; + if (this._preLaunched.sharedSocksProxy) { + // Note: tethering client configures the pattern, and connected client's pattern is ignored. + playwright.options.socksProxyPort = this._preLaunched.sharedSocksProxy.port(); + this._debugLog(`using shared socks proxy on port ${playwright.options.socksProxyPort}`); + } else { + ownedSocksProxy = await this._createOwnedSocksProxy(playwright); + } + const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions); this._cleanups.push(async () => { @@ -131,18 +148,24 @@ export class PlaywrightConnection { this.close({ code: 1001, reason: 'Browser closed' }); }); - return new PlaywrightDispatcher(scope, playwright, socksProxy, browser); + return new PlaywrightDispatcher(scope, playwright, ownedSocksProxy, browser); } private async _initPreLaunchedBrowserMode(scope: RootDispatcher) { this._debugLog(`engaged pre-launched (browser) mode`); const playwright = this._preLaunched.playwright!; + + // Note: connected client owns the socks proxy and configures the pattern. + playwright.options.socksProxyPort = this._preLaunched.ownedSocksProxy?.port(); + this._preLaunched.ownedSocksProxy?.setPattern(this._options.socksProxyPattern); + const browser = this._preLaunched.browser!; browser.on(Browser.Events.Disconnected, () => { // Underlying browser did close for some reason - force disconnect the client. this.close({ code: 1001, reason: 'Browser closed' }); }); - const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, browser); + + const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, this._preLaunched.ownedSocksProxy, browser); // In pre-launched mode, keep only the pre-launched browser. for (const b of playwright.allBrowsers()) { if (b !== browser) @@ -175,6 +198,11 @@ export class PlaywrightConnection { private async _initReuseBrowsersMode(scope: RootDispatcher) { this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`); const playwright = this._preLaunched.playwright!; + + // Note: connected client owns the socks proxy and configures the pattern. + playwright.options.socksProxyPort = this._preLaunched.sharedSocksProxy?.port(); + this._debugLog(`using shared socks proxy on port ${playwright.options.socksProxyPort}`); + const requestedOptions = launchOptionsHash(this._options.launchOptions); let browser = playwright.allBrowsers().find(b => { if (b.options.name !== this._options.browserName) @@ -221,16 +249,9 @@ export class PlaywrightConnection { return playwrightDispatcher; } - private async _configureSocksProxy(playwright: Playwright): Promise { - if (!this._options.socksProxy) - return undefined; - if (this._preLaunched.networkTetheringSocksProxy) { - playwright.options.socksProxyPort = this._preLaunched.networkTetheringSocksProxy.port(); - this._debugLog(`using network tether proxy on port ${playwright.options.socksProxyPort}`); - return undefined; - } + private async _createOwnedSocksProxy(playwright: Playwright): Promise { const socksProxy = new SocksProxy(); - socksProxy.setProxyPattern(this._options.socksProxy); + socksProxy.setPattern(this._options.socksProxyPattern); playwright.options.socksProxyPort = await socksProxy.listen(0); this._debugLog(`started socks proxy on port ${playwright.options.socksProxyPort}`); this._cleanups.push(() => socksProxy.close()); diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index 2ac7ee9866..3af284a4e6 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -40,9 +40,10 @@ function newLogger() { type ServerOptions = { path: string; maxConnections: number; - preLaunchedBrowser?: Browser - preLaunchedAndroidDevice?: AndroidDevice - browserProxyMode: 'client' | 'tether' | 'disabled', + preLaunchedBrowser?: Browser; + preLaunchedAndroidDevice?: AndroidDevice; + preLaunchedSocksProxy?: SocksProxy; + browserProxyMode: 'client' | 'tether'; ownedByTetherClient?: boolean; }; @@ -61,12 +62,6 @@ export class PlaywrightServer { this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android._playwrightOptions.rootSdkObject as Playwright; } - preLaunchedPlaywright(): Playwright { - if (!this._preLaunchedPlaywright) - this._preLaunchedPlaywright = createPlaywright('javascript'); - return this._preLaunchedPlaywright; - } - async listen(port: number = 0): Promise { const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { if (request.method === 'GET' && request.url === '/json') { @@ -115,7 +110,6 @@ export class PlaywrightServer { const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null; const proxyHeader = request.headers['x-playwright-proxy']; const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader); - const socksProxy = this._options.browserProxyMode !== 'disabled' ? proxyValue : undefined; const launchOptionsHeader = request.headers['x-playwright-launch-options'] || ''; let launchOptions: LaunchOptions = {}; @@ -131,9 +125,11 @@ export class PlaywrightServer { const shouldReuseBrowser = !!request.headers['x-playwright-reuse-context']; // If we started in the legacy reuse-browser mode, create this._preLaunchedPlaywright. - // If we get a reuse-controller request, create this._preLaunchedPlaywright. - if (isDebugControllerClient || shouldReuseBrowser) - this.preLaunchedPlaywright(); + // If we get a reuse-controller request, create this._preLaunchedPlaywright. + if (isDebugControllerClient || shouldReuseBrowser) { + if (!this._preLaunchedPlaywright) + this._preLaunchedPlaywright = createPlaywright('javascript'); + } let clientType: ClientType = 'playwright'; let semaphore: Semaphore = browserSemaphore; @@ -147,7 +143,7 @@ export class PlaywrightServer { clientType = 'reuse-browser'; semaphore = reuseBrowserSemaphore; } else if (this._options.preLaunchedBrowser || this._options.preLaunchedAndroidDevice) { - clientType = 'pre-launched-browser'; + clientType = 'pre-launched-browser-or-android'; semaphore = browserSemaphore; } else if (browserName) { clientType = 'launch-browser'; @@ -160,12 +156,13 @@ export class PlaywrightServer { const connection = new PlaywrightConnection( semaphore.aquire(), clientType, ws, - { socksProxy, browserName, launchOptions }, + { socksProxyPattern: proxyValue, browserName, launchOptions }, { playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser, androidDevice: this._options.preLaunchedAndroidDevice, - networkTetheringSocksProxy: this._networkTetheringSocksProxy, + ownedSocksProxy: this._options.preLaunchedSocksProxy, + sharedSocksProxy: this._networkTetheringSocksProxy, }, log, () => { semaphore.release();