feat(tether): always enable socks proxy on the server (#19363)

This commit is contained in:
Dmitry Gozman 2022-12-08 14:23:14 -08:00 committed by GitHub
parent 92b3995101
commit 465278a54f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 46 deletions

View file

@ -49,7 +49,7 @@ export class AndroidServerLauncherImpl {
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`; const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
// 2. Start the server // 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); const wsEndpoint = await server.listen(options.port);
// 3. Return the BrowserServer interface // 3. Return the BrowserServer interface

View file

@ -26,6 +26,7 @@ import { createPlaywright } from './server/playwright';
import { PlaywrightServer } from './remote/playwrightServer'; import { PlaywrightServer } from './remote/playwrightServer';
import { helper } from './server/helper'; import { helper } from './server/helper';
import { rewriteErrorMessage } from './utils/stackTrace'; import { rewriteErrorMessage } from './utils/stackTrace';
import { SocksProxy } from './common/socksProxy';
export class BrowserServerLauncherImpl implements BrowserServerLauncher { export class BrowserServerLauncherImpl implements BrowserServerLauncher {
private _browserName: 'chromium' | 'firefox' | 'webkit'; private _browserName: 'chromium' | 'firefox' | 'webkit';
@ -36,6 +37,9 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
const playwright = createPlaywright('javascript'); const playwright = createPlaywright('javascript');
const socksProxy = new SocksProxy();
playwright.options.socksProxyPort = await socksProxy.listen(0);
// 1. Pre-launch the browser // 1. Pre-launch the browser
const metadata = serverSideCallMetadata(); const metadata = serverSideCallMetadata();
const browser = await playwright[this._browserName].launch(metadata, { 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()}`; const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
// 2. Start the server // 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); const wsEndpoint = await server.listen(options.port);
// 3. Return the BrowserServer interface // 3. Return the BrowserServer interface
@ -63,7 +67,8 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
browserServer.kill = () => browser.options.browserProcess.kill(); browserServer.kill = () => browser.options.browserProcess.kill();
(browserServer as any)._disconnectForTest = () => server.close(); (browserServer as any)._disconnectForTest = () => server.close();
(browserServer as any)._userDataDirForTest = (browser as any)._userDataDirForTest; (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(); server.close();
browserServer.emit('close', exitCode, signal); browserServer.emit('close', exitCode, signal);
}; };

View file

@ -270,7 +270,7 @@ program
.option('--port <port>', 'Server port') .option('--port <port>', 'Server port')
.option('--path <path>', 'Endpoint Path', '/') .option('--path <path>', 'Endpoint Path', '/')
.option('--max-clients <maxClients>', 'Maximum clients') .option('--max-clients <maxClients>', 'Maximum clients')
.option('--proxy-mode <mode>', 'Either `client`, `tether` or `disabled`. Defaults to `client`.', 'client') .option('--proxy-mode <mode>', 'Either `client` or `tether`. Defaults to `client`.', 'client')
.action(function(options) { .action(function(options) {
runServer({ runServer({
port: options.port ? +options.port : undefined, port: options.port ? +options.port : undefined,

View file

@ -50,7 +50,7 @@ export type RunServerOptions = {
port?: number, port?: number,
path?: string, path?: string,
maxConnections?: number, maxConnections?: number,
browserProxyMode?: 'client' | 'tether' | 'disabled', browserProxyMode?: 'client' | 'tether',
ownedByTetherClient?: boolean, ownedByTetherClient?: boolean,
}; };

View file

@ -317,7 +317,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
}); });
} }
setProxyPattern(pattern: string | undefined) { setPattern(pattern: string | undefined) {
this._pattern = pattern; this._pattern = pattern;
} }
@ -364,6 +364,8 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
} }
async close() { async close() {
if (this._closed)
return;
this._closed = true; this._closed = true;
for (const socket of this._sockets) for (const socket of this._sockets)
socket.destroy(); socket.destroy();

View file

@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
const log = debug(`pw:grid:worker:${workerId}`); const log = debug(`pw:grid:worker:${workerId}`);
log('created'); log('created');
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`); 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'); log('exiting process');
setTimeout(() => process.exit(0), 30000); setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers. // Meanwhile, try to gracefully close all browsers.

View file

@ -25,10 +25,10 @@ import type { LaunchOptions } from '../server/types';
import { AndroidDevice } from '../server/android/android'; import { AndroidDevice } from '../server/android/android';
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher'; 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 = { type Options = {
socksProxy: string | undefined, socksProxyPattern: string | undefined,
browserName: string | null, browserName: string | null,
launchOptions: LaunchOptions, launchOptions: LaunchOptions,
}; };
@ -37,7 +37,8 @@ type PreLaunched = {
playwright?: Playwright | undefined; playwright?: Playwright | undefined;
browser?: Browser | undefined; browser?: Browser | undefined;
androidDevice?: AndroidDevice | undefined; androidDevice?: AndroidDevice | undefined;
networkTetheringSocksProxy?: SocksProxy | undefined; ownedSocksProxy?: SocksProxy | undefined;
sharedSocksProxy?: SocksProxy | undefined;
}; };
export class PlaywrightConnection { export class PlaywrightConnection {
@ -55,9 +56,9 @@ export class PlaywrightConnection {
this._ws = ws; this._ws = ws;
this._preLaunched = preLaunched; this._preLaunched = preLaunched;
this._options = options; this._options = options;
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser') if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android')
assert(preLaunched.playwright); assert(preLaunched.playwright);
if (clientType === 'pre-launched-browser') if (clientType === 'pre-launched-browser-or-android')
assert(preLaunched.browser || preLaunched.androidDevice); assert(preLaunched.browser || preLaunched.androidDevice);
this._onClose = onClose; this._onClose = onClose;
this._debugLog = log; this._debugLog = log;
@ -84,7 +85,7 @@ export class PlaywrightConnection {
this._root = new RootDispatcher(this._dispatcherConnection, async scope => { this._root = new RootDispatcher(this._dispatcherConnection, async scope => {
if (clientType === 'reuse-browser') if (clientType === 'reuse-browser')
return await this._initReuseBrowsersMode(scope); 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); return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
if (clientType === 'launch-browser') if (clientType === 'launch-browser')
return await this._initLaunchBrowserMode(scope); return await this._initLaunchBrowserMode(scope);
@ -99,8 +100,9 @@ export class PlaywrightConnection {
private async _initPlaywrightTetheringMode(scope: RootDispatcher) { private async _initPlaywrightTetheringMode(scope: RootDispatcher) {
this._debugLog(`engaged playwright.tethering mode`); this._debugLog(`engaged playwright.tethering mode`);
const playwright = createPlaywright('javascript'); const playwright = createPlaywright('javascript');
this._preLaunched.networkTetheringSocksProxy?.setProxyPattern(this._options.socksProxy); this._preLaunched.sharedSocksProxy?.setPattern(this._options.socksProxyPattern);
return new PlaywrightDispatcher(scope, playwright, this._preLaunched.networkTetheringSocksProxy); // Tethering client owns the shared socks proxy.
return new PlaywrightDispatcher(scope, playwright, this._preLaunched.sharedSocksProxy);
} }
private async _initPlaywrightConnectMode(scope: RootDispatcher) { private async _initPlaywrightConnectMode(scope: RootDispatcher) {
@ -111,15 +113,30 @@ export class PlaywrightConnection {
await Promise.all(playwright.allBrowsers().map(browser => browser.close())); await Promise.all(playwright.allBrowsers().map(browser => browser.close()));
}); });
const socksProxy = await this._configureSocksProxy(playwright); let ownedSocksProxy: SocksProxy | undefined;
return new PlaywrightDispatcher(scope, playwright, socksProxy); 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) { private async _initLaunchBrowserMode(scope: RootDispatcher) {
this._debugLog(`engaged launch mode for "${this._options.browserName}"`); this._debugLog(`engaged launch mode for "${this._options.browserName}"`);
const playwright = createPlaywright('javascript'); 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); const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
this._cleanups.push(async () => { this._cleanups.push(async () => {
@ -131,18 +148,24 @@ export class PlaywrightConnection {
this.close({ code: 1001, reason: 'Browser closed' }); 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) { private async _initPreLaunchedBrowserMode(scope: RootDispatcher) {
this._debugLog(`engaged pre-launched (browser) mode`); this._debugLog(`engaged pre-launched (browser) mode`);
const playwright = this._preLaunched.playwright!; 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!; const browser = this._preLaunched.browser!;
browser.on(Browser.Events.Disconnected, () => { browser.on(Browser.Events.Disconnected, () => {
// Underlying browser did close for some reason - force disconnect the client. // Underlying browser did close for some reason - force disconnect the client.
this.close({ code: 1001, reason: 'Browser closed' }); 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. // In pre-launched mode, keep only the pre-launched browser.
for (const b of playwright.allBrowsers()) { for (const b of playwright.allBrowsers()) {
if (b !== browser) if (b !== browser)
@ -175,6 +198,11 @@ export class PlaywrightConnection {
private async _initReuseBrowsersMode(scope: RootDispatcher) { private async _initReuseBrowsersMode(scope: RootDispatcher) {
this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`); this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`);
const playwright = this._preLaunched.playwright!; 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); const requestedOptions = launchOptionsHash(this._options.launchOptions);
let browser = playwright.allBrowsers().find(b => { let browser = playwright.allBrowsers().find(b => {
if (b.options.name !== this._options.browserName) if (b.options.name !== this._options.browserName)
@ -221,16 +249,9 @@ export class PlaywrightConnection {
return playwrightDispatcher; return playwrightDispatcher;
} }
private async _configureSocksProxy(playwright: Playwright): Promise<undefined|SocksProxy> { private async _createOwnedSocksProxy(playwright: Playwright): Promise<SocksProxy | undefined> {
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;
}
const socksProxy = new SocksProxy(); const socksProxy = new SocksProxy();
socksProxy.setProxyPattern(this._options.socksProxy); socksProxy.setPattern(this._options.socksProxyPattern);
playwright.options.socksProxyPort = await socksProxy.listen(0); playwright.options.socksProxyPort = await socksProxy.listen(0);
this._debugLog(`started socks proxy on port ${playwright.options.socksProxyPort}`); this._debugLog(`started socks proxy on port ${playwright.options.socksProxyPort}`);
this._cleanups.push(() => socksProxy.close()); this._cleanups.push(() => socksProxy.close());

View file

@ -40,9 +40,10 @@ function newLogger() {
type ServerOptions = { type ServerOptions = {
path: string; path: string;
maxConnections: number; maxConnections: number;
preLaunchedBrowser?: Browser preLaunchedBrowser?: Browser;
preLaunchedAndroidDevice?: AndroidDevice preLaunchedAndroidDevice?: AndroidDevice;
browserProxyMode: 'client' | 'tether' | 'disabled', preLaunchedSocksProxy?: SocksProxy;
browserProxyMode: 'client' | 'tether';
ownedByTetherClient?: boolean; ownedByTetherClient?: boolean;
}; };
@ -61,12 +62,6 @@ export class PlaywrightServer {
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android._playwrightOptions.rootSdkObject as Playwright; 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<string> { async listen(port: number = 0): Promise<string> {
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
if (request.method === 'GET' && request.url === '/json') { 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 browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
const proxyHeader = request.headers['x-playwright-proxy']; const proxyHeader = request.headers['x-playwright-proxy'];
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader); 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'] || ''; const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
let launchOptions: LaunchOptions = {}; let launchOptions: LaunchOptions = {};
@ -131,9 +125,11 @@ export class PlaywrightServer {
const shouldReuseBrowser = !!request.headers['x-playwright-reuse-context']; const shouldReuseBrowser = !!request.headers['x-playwright-reuse-context'];
// If we started in the legacy reuse-browser mode, create this._preLaunchedPlaywright. // If we started in the legacy reuse-browser mode, create this._preLaunchedPlaywright.
// If we get a reuse-controller request, create this._preLaunchedPlaywright. // If we get a reuse-controller request, create this._preLaunchedPlaywright.
if (isDebugControllerClient || shouldReuseBrowser) if (isDebugControllerClient || shouldReuseBrowser) {
this.preLaunchedPlaywright(); if (!this._preLaunchedPlaywright)
this._preLaunchedPlaywright = createPlaywright('javascript');
}
let clientType: ClientType = 'playwright'; let clientType: ClientType = 'playwright';
let semaphore: Semaphore = browserSemaphore; let semaphore: Semaphore = browserSemaphore;
@ -147,7 +143,7 @@ export class PlaywrightServer {
clientType = 'reuse-browser'; clientType = 'reuse-browser';
semaphore = reuseBrowserSemaphore; semaphore = reuseBrowserSemaphore;
} else if (this._options.preLaunchedBrowser || this._options.preLaunchedAndroidDevice) { } else if (this._options.preLaunchedBrowser || this._options.preLaunchedAndroidDevice) {
clientType = 'pre-launched-browser'; clientType = 'pre-launched-browser-or-android';
semaphore = browserSemaphore; semaphore = browserSemaphore;
} else if (browserName) { } else if (browserName) {
clientType = 'launch-browser'; clientType = 'launch-browser';
@ -160,12 +156,13 @@ export class PlaywrightServer {
const connection = new PlaywrightConnection( const connection = new PlaywrightConnection(
semaphore.aquire(), semaphore.aquire(),
clientType, ws, clientType, ws,
{ socksProxy, browserName, launchOptions }, { socksProxyPattern: proxyValue, browserName, launchOptions },
{ {
playwright: this._preLaunchedPlaywright, playwright: this._preLaunchedPlaywright,
browser: this._options.preLaunchedBrowser, browser: this._options.preLaunchedBrowser,
androidDevice: this._options.preLaunchedAndroidDevice, androidDevice: this._options.preLaunchedAndroidDevice,
networkTetheringSocksProxy: this._networkTetheringSocksProxy, ownedSocksProxy: this._options.preLaunchedSocksProxy,
sharedSocksProxy: this._networkTetheringSocksProxy,
}, },
log, () => { log, () => {
semaphore.release(); semaphore.release();