mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-05 00:48:37 +03:00
feat(connect): allow exposing local network to the remote browser (experimental) (#19372)
`connectOptions: { _exposeNetwork: '*' | 'localhost' }` References #19287.
This commit is contained in:
parent
8ea48752db
commit
256e9fd443
@ -69,7 +69,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||
|
||||
async launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||
if (this._defaultConnectOptions)
|
||||
return await this._connectInsteadOfLaunching();
|
||||
return await this._connectInsteadOfLaunching(this._defaultConnectOptions);
|
||||
|
||||
const logger = options.logger || this._defaultLaunchOptions?.logger;
|
||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
@ -89,14 +89,15 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||
});
|
||||
}
|
||||
|
||||
private async _connectInsteadOfLaunching(): Promise<Browser> {
|
||||
const connectOptions = this._defaultConnectOptions!;
|
||||
return this._connect(connectOptions.wsEndpoint, {
|
||||
private async _connectInsteadOfLaunching(connectOptions: ConnectOptions): Promise<Browser> {
|
||||
return this._connect({
|
||||
wsEndpoint: connectOptions.wsEndpoint,
|
||||
headers: {
|
||||
'x-playwright-browser': this.name(),
|
||||
'x-playwright-launch-options': JSON.stringify(this._defaultLaunchOptions || {}),
|
||||
...connectOptions.headers,
|
||||
},
|
||||
_exposeNetwork: connectOptions._exposeNetwork,
|
||||
slowMo: connectOptions.slowMo,
|
||||
timeout: connectOptions.timeout ?? 3 * 60 * 1000, // 3 minutes
|
||||
});
|
||||
}
|
||||
@ -132,22 +133,28 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||
});
|
||||
}
|
||||
|
||||
connect(options: api.ConnectOptions & { wsEndpoint?: string }): Promise<api.Browser>;
|
||||
connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise<api.Browser>;
|
||||
connect(wsEndpoint: string, options?: api.ConnectOptions): Promise<api.Browser>;
|
||||
async connect(optionsOrWsEndpoint: string|(api.ConnectOptions & { wsEndpoint?: string }), options?: api.ConnectOptions): Promise<Browser>{
|
||||
async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint: string }), options?: api.ConnectOptions): Promise<Browser>{
|
||||
if (typeof optionsOrWsEndpoint === 'string')
|
||||
return this._connect(optionsOrWsEndpoint, options);
|
||||
return this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint });
|
||||
assert(optionsOrWsEndpoint.wsEndpoint, 'options.wsEndpoint is required');
|
||||
return this._connect(optionsOrWsEndpoint.wsEndpoint, optionsOrWsEndpoint);
|
||||
return this._connect(optionsOrWsEndpoint);
|
||||
}
|
||||
|
||||
async _connect(wsEndpoint: string, params: Partial<ConnectOptions> = {}): Promise<Browser> {
|
||||
async _connect(params: ConnectOptions): Promise<Browser> {
|
||||
const logger = params.logger;
|
||||
return await this._wrapApiCall(async () => {
|
||||
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
|
||||
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
|
||||
const localUtils = this._connection.localUtils();
|
||||
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: params.slowMo, timeout: params.timeout };
|
||||
const connectParams: channels.LocalUtilsConnectParams = {
|
||||
wsEndpoint: params.wsEndpoint,
|
||||
headers,
|
||||
exposeNetwork: params._exposeNetwork,
|
||||
slowMo: params.slowMo,
|
||||
timeout: params.timeout,
|
||||
};
|
||||
if ((params as any).__testHookRedirectPortForwarding)
|
||||
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
|
||||
const { pipe } = await localUtils._channel.connect(connectParams);
|
||||
|
@ -87,6 +87,7 @@ export type LaunchPersistentContextOptions = Omit<LaunchOptionsBase & BrowserCon
|
||||
export type ConnectOptions = {
|
||||
wsEndpoint: string,
|
||||
headers?: { [key: string]: string; };
|
||||
_exposeNetwork?: string,
|
||||
slowMo?: number,
|
||||
timeout?: number,
|
||||
logger?: Logger,
|
||||
|
@ -431,19 +431,31 @@ export class SocksProxyHandler extends EventEmitter {
|
||||
};
|
||||
|
||||
private _sockets = new Map<string, net.Socket>();
|
||||
private _pattern: string | undefined;
|
||||
private _redirectPortForTest: number | undefined;
|
||||
|
||||
constructor(redirectPortForTest?: number) {
|
||||
constructor(pattern: string | undefined, redirectPortForTest?: number) {
|
||||
super();
|
||||
this._pattern = pattern;
|
||||
this._redirectPortForTest = redirectPortForTest;
|
||||
}
|
||||
|
||||
private _matchesPattern(host: string, port: number) {
|
||||
return this._pattern === '*' || (this._pattern === 'localhost' && host === 'localhost');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const uid of this._sockets.keys())
|
||||
this.socketClosed({ uid });
|
||||
}
|
||||
|
||||
async socketRequested({ uid, host, port }: SocksSocketRequestedPayload): Promise<void> {
|
||||
if (!this._matchesPattern(host, port)) {
|
||||
const payload: SocksSocketFailedPayload = { uid, errorCode: 'ECONNREFUSED' };
|
||||
this.emit(SocksProxyHandler.Events.SocksFailed, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (host === 'local.playwright')
|
||||
host = '127.0.0.1';
|
||||
// Node.js 17 does resolve localhost to ipv6
|
||||
|
@ -288,7 +288,7 @@ async function tetherHostNetwork(endpoint: string) {
|
||||
'x-playwright-proxy': '*',
|
||||
};
|
||||
const transport = await WebSocketTransport.connect(undefined /* progress */, wsEndpoint, headers, true /* followRedirects */);
|
||||
const socksInterceptor = new SocksInterceptor(transport, undefined);
|
||||
const socksInterceptor = new SocksInterceptor(transport, '*', undefined);
|
||||
transport.onmessage = json => socksInterceptor.interceptMessage(json);
|
||||
transport.onclose = () => {
|
||||
socksInterceptor.cleanup();
|
||||
@ -387,7 +387,7 @@ export function addDockerCLI(program: Command) {
|
||||
.option('--browser <name>', 'browser to launch')
|
||||
.option('--endpoint <url>', 'server endpoint')
|
||||
.action(async function(options: { browser: string, endpoint: string }) {
|
||||
let browserType: any;
|
||||
let browserType: playwright.BrowserType | undefined;
|
||||
if (options.browser === 'chromium')
|
||||
browserType = playwright.chromium;
|
||||
else if (options.browser === 'firefox')
|
||||
@ -404,9 +404,9 @@ export function addDockerCLI(program: Command) {
|
||||
headless: false,
|
||||
viewport: null,
|
||||
}),
|
||||
'x-playwright-proxy': '*',
|
||||
},
|
||||
});
|
||||
_exposeNetwork: '*',
|
||||
} as any);
|
||||
const context = await browser.newContext();
|
||||
context.on('page', (page: playwright.Page) => {
|
||||
page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed.
|
||||
|
@ -248,6 +248,7 @@ scheme.LocalUtilsHarUnzipResult = tOptional(tObject({}));
|
||||
scheme.LocalUtilsConnectParams = tObject({
|
||||
wsEndpoint: tString,
|
||||
headers: tOptional(tAny),
|
||||
exposeNetwork: tOptional(tString),
|
||||
slowMo: tOptional(tNumber),
|
||||
timeout: tOptional(tNumber),
|
||||
socksProxyRedirectPortForTest: tOptional(tNumber),
|
||||
|
@ -156,11 +156,15 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||
const controller = new ProgressController(metadata, this._object as SdkObject);
|
||||
controller.setLogName('browser');
|
||||
return await controller.run(async progress => {
|
||||
const paramsHeaders = Object.assign({ 'User-Agent': getUserAgent() }, params.headers || {});
|
||||
const wsHeaders = {
|
||||
'User-Agent': getUserAgent(),
|
||||
'x-playwright-proxy': params.exposeNetwork ?? '',
|
||||
...params.headers,
|
||||
};
|
||||
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
|
||||
|
||||
const transport = await WebSocketTransport.connect(progress, wsEndpoint, paramsHeaders, true);
|
||||
const socksInterceptor = new SocksInterceptor(transport, params.socksProxyRedirectPortForTest);
|
||||
const transport = await WebSocketTransport.connect(progress, wsEndpoint, wsHeaders, true);
|
||||
const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest);
|
||||
const pipe = new JsonPipeDispatcher(this);
|
||||
transport.onmessage = json => {
|
||||
if (socksInterceptor.interceptMessage(json))
|
||||
|
@ -27,8 +27,8 @@ export class SocksInterceptor {
|
||||
private _socksSupportObjectGuid?: string;
|
||||
private _ids = new Set<number>();
|
||||
|
||||
constructor(transport: WebSocketTransport, redirectPortForTest: number | undefined) {
|
||||
this._handler = new socks.SocksProxyHandler(redirectPortForTest);
|
||||
constructor(transport: WebSocketTransport, pattern: string | undefined, redirectPortForTest: number | undefined) {
|
||||
this._handler = new socks.SocksProxyHandler(pattern, redirectPortForTest);
|
||||
|
||||
let lastId = -1;
|
||||
this._channel = new Proxy(new EventEmitter(), {
|
||||
|
@ -87,8 +87,9 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||
}
|
||||
return use({
|
||||
wsEndpoint,
|
||||
headers
|
||||
});
|
||||
headers,
|
||||
_exposeNetwork: process.env.PW_TEST_CONNECT_EXPOSE_NETWORK,
|
||||
} as any);
|
||||
}, { scope: 'worker', option: true }],
|
||||
screenshot: ['off', { scope: 'worker', option: true }],
|
||||
video: ['off', { scope: 'worker', option: true }],
|
||||
|
@ -35,9 +35,7 @@ export const dockerPlugin: TestRunnerPlugin = {
|
||||
throw new Error('ERROR: please launch docker container separately!');
|
||||
println('');
|
||||
process.env.PW_TEST_CONNECT_WS_ENDPOINT = info.httpEndpoint;
|
||||
process.env.PW_TEST_CONNECT_HEADERS = JSON.stringify({
|
||||
'x-playwright-proxy': '*',
|
||||
});
|
||||
process.env.PW_TEST_CONNECT_EXPOSE_NETWORK = '*';
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -446,12 +446,14 @@ export type LocalUtilsHarUnzipResult = void;
|
||||
export type LocalUtilsConnectParams = {
|
||||
wsEndpoint: string,
|
||||
headers?: any,
|
||||
exposeNetwork?: string,
|
||||
slowMo?: number,
|
||||
timeout?: number,
|
||||
socksProxyRedirectPortForTest?: number,
|
||||
};
|
||||
export type LocalUtilsConnectOptions = {
|
||||
headers?: any,
|
||||
exposeNetwork?: string,
|
||||
slowMo?: number,
|
||||
timeout?: number,
|
||||
socksProxyRedirectPortForTest?: number,
|
||||
|
@ -535,6 +535,7 @@ LocalUtils:
|
||||
parameters:
|
||||
wsEndpoint: string
|
||||
headers: json?
|
||||
exposeNetwork: string?
|
||||
slowMo: number?
|
||||
timeout: number?
|
||||
socksProxyRedirectPortForTest: number?
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { BrowserType, Browser, LaunchOptions } from 'playwright-core';
|
||||
import type { BrowserType, Browser } from 'playwright-core';
|
||||
import type { CommonFixtures, TestChildProcess } from './commonFixtures';
|
||||
|
||||
export interface PlaywrightServer {
|
||||
@ -79,7 +79,7 @@ export class RemoteServer implements PlaywrightServer {
|
||||
const browserOptions = (browserType as any)._defaultLaunchOptions;
|
||||
// Copy options to prevent a large JSON string when launching subprocess.
|
||||
// Otherwise, we get `Error: spawn ENAMETOOLONG` on Windows.
|
||||
const launchOptions: LaunchOptions = {
|
||||
const launchOptions: Parameters<BrowserType['launchServer']>[0] = {
|
||||
args: browserOptions.args,
|
||||
headless: browserOptions.headless,
|
||||
channel: browserOptions.channel,
|
||||
|
@ -679,7 +679,6 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
test.describe('socks proxy', () => {
|
||||
test.fixme(({ platform, browserName }) => browserName === 'webkit' && platform === 'win32');
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
test.skip(kind === 'launchServer', 'This feature is not yet supported in launchServer');
|
||||
|
||||
test('should forward non-forwarded requests', async ({ server, startRemoteServer, connect }) => {
|
||||
let reachedOriginalTarget = false;
|
||||
@ -688,9 +687,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
res.end('<html><body>original-target</body></html>');
|
||||
});
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
headers: { 'x-playwright-proxy': '*' }
|
||||
});
|
||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any);
|
||||
const page = await browser.newPage();
|
||||
await page.goto(server.PREFIX + '/foo.html');
|
||||
expect(await page.content()).toContain('original-target');
|
||||
@ -707,9 +704,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
});
|
||||
const examplePort = 20_000 + testInfo.workerIndex * 3;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
headers: { 'x-playwright-proxy': '*' }
|
||||
}, dummyServerPort);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort);
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://127.0.0.1:${examplePort}/foo.html`);
|
||||
expect(await page.content()).toContain('from-dummy-server');
|
||||
@ -726,9 +721,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
});
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
headers: { 'x-playwright-proxy': '*' }
|
||||
}, dummyServerPort);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort);
|
||||
const page = await browser.newPage();
|
||||
const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`);
|
||||
expect(response.status()).toBe(200);
|
||||
@ -744,9 +737,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
});
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
headers: { 'x-playwright-proxy': '*' }
|
||||
}, dummyServerPort);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort);
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://local.playwright:${examplePort}/foo.html`);
|
||||
expect(await page.content()).toContain('from-dummy-server');
|
||||
@ -756,9 +747,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
test('should lead to the error page for forwarded requests when the connection is refused', async ({ connect, startRemoteServer, browserName }, workerInfo) => {
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
headers: { 'x-playwright-proxy': '*' }
|
||||
});
|
||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any);
|
||||
const page = await browser.newPage();
|
||||
const error = await page.goto(`http://127.0.0.1:${examplePort}`).catch(e => e);
|
||||
if (browserName === 'chromium')
|
||||
@ -779,9 +768,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
});
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
headers: { 'x-playwright-proxy': 'localhost' }
|
||||
}, dummyServerPort);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: 'localhost' } as any, dummyServerPort);
|
||||
const page = await browser.newPage();
|
||||
|
||||
// localhost should be proxied.
|
||||
@ -801,6 +788,30 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||
});
|
||||
expect(failed).toBe(true);
|
||||
});
|
||||
|
||||
test('should check proxy pattern on the client', async ({ connect, startRemoteServer, server, browserName, platform, dummyServerPort }, workerInfo) => {
|
||||
let reachedOriginalTarget = false;
|
||||
server.setRoute('/foo.html', async (req, res) => {
|
||||
reachedOriginalTarget = true;
|
||||
res.end('<html><body>from-original-server</body></html>');
|
||||
});
|
||||
const remoteServer = await startRemoteServer(kind);
|
||||
const browser = await connect(remoteServer.wsEndpoint(), {
|
||||
_exposeNetwork: 'localhost',
|
||||
headers: {
|
||||
'x-playwright-proxy': '*',
|
||||
},
|
||||
} as any, dummyServerPort);
|
||||
const page = await browser.newPage();
|
||||
|
||||
// 127.0.0.1 should fail on the client side.
|
||||
let failed = false;
|
||||
await page.goto(`http://127.0.0.1:${server.PORT}/foo.html`).catch(e => {
|
||||
failed = true;
|
||||
});
|
||||
expect(failed).toBe(true);
|
||||
expect(reachedOriginalTarget).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user