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()}`;
// 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

View File

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

View File

@ -270,7 +270,7 @@ program
.option('--port <port>', 'Server port')
.option('--path <path>', 'Endpoint Path', '/')
.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) {
runServer({
port: options.port ? +options.port : undefined,

View File

@ -50,7 +50,7 @@ export type RunServerOptions = {
port?: number,
path?: string,
maxConnections?: number,
browserProxyMode?: 'client' | 'tether' | 'disabled',
browserProxyMode?: 'client' | 'tether',
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;
}
@ -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();

View File

@ -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.

View File

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

View File

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