feat(browserServer): forward local ports (#6375)
This commit is contained in:
parent
c9f35fb811
commit
3f43db5cc4
|
|
@ -35,6 +35,9 @@ import { BrowserContext } from './server/browserContext';
|
|||
import { CRBrowser } from './server/chromium/crBrowser';
|
||||
import { CDPSessionDispatcher } from './dispatchers/cdpSessionDispatcher';
|
||||
import { PageDispatcher } from './dispatchers/pageDispatcher';
|
||||
import { BrowserServerPortForwardingServer } from './server/socksSocket';
|
||||
import { SocksSocketDispatcher } from './dispatchers/socksSocketDispatcher';
|
||||
import { SocksInterceptedSocketHandler } from './server/socksServer';
|
||||
|
||||
export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
||||
private _playwright: Playwright;
|
||||
|
|
@ -46,20 +49,24 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
}
|
||||
|
||||
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
|
||||
const portForwardingServer = new BrowserServerPortForwardingServer(this._playwright, !!options._acceptForwardedPorts);
|
||||
// 1. Pre-launch the browser
|
||||
const browser = await this._browserType.launch(internalCallMetadata(), {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? envObjectToArray(options.env) : undefined,
|
||||
...portForwardingServer.browserLaunchOptions(),
|
||||
}, toProtocolLogger(options.logger));
|
||||
|
||||
// 2. Start the server
|
||||
const delegate: PlaywrightServerDelegate = {
|
||||
path: '/' + createGuid(),
|
||||
allowMultipleClients: true,
|
||||
onClose: () => {},
|
||||
onConnect: this._onConnect.bind(this, browser),
|
||||
allowMultipleClients: options._acceptForwardedPorts ? false : true,
|
||||
onClose: () => {
|
||||
portForwardingServer.stop();
|
||||
},
|
||||
onConnect: this._onConnect.bind(this, browser, portForwardingServer),
|
||||
};
|
||||
const server = new PlaywrightServer(delegate);
|
||||
const wsEndpoint = await server.listen(options.port);
|
||||
|
|
@ -78,7 +85,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
return browserServer;
|
||||
}
|
||||
|
||||
private _onConnect(browser: Browser, scope: DispatcherScope, forceDisconnect: () => void) {
|
||||
private _onConnect(browser: Browser, portForwardingServer: BrowserServerPortForwardingServer, scope: DispatcherScope, forceDisconnect: () => void) {
|
||||
const selectors = new Selectors();
|
||||
const selectorsDispatcher = new SelectorsDispatcher(scope, selectors);
|
||||
const browserDispatcher = new ConnectedBrowserDispatcher(scope, browser, selectors);
|
||||
|
|
@ -86,8 +93,16 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
// Underlying browser did close for some reason - force disconnect the client.
|
||||
forceDisconnect();
|
||||
});
|
||||
new PlaywrightDispatcher(scope, this._playwright, selectorsDispatcher, browserDispatcher);
|
||||
const playwrightDispatcher = new PlaywrightDispatcher(scope, this._playwright, selectorsDispatcher, browserDispatcher, (ports: number[]) => {
|
||||
portForwardingServer.enablePortForwarding(ports);
|
||||
});
|
||||
const incomingSocksSocketHandler = (socket: SocksInterceptedSocketHandler) => {
|
||||
playwrightDispatcher._dispatchEvent('incomingSocksSocket', { socket: new SocksSocketDispatcher(playwrightDispatcher._scope, socket) });
|
||||
};
|
||||
portForwardingServer.on('incomingSocksSocket', incomingSocksSocketHandler);
|
||||
|
||||
return () => {
|
||||
portForwardingServer.off('incomingSocksSocket', incomingSocksSocketHandler);
|
||||
// Cleanup contexts upon disconnect.
|
||||
browserDispatcher.cleanupContexts().catch(e => {});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
if (!connection.isDisconnected())
|
||||
connection.dispatch(JSON.parse(event.data));
|
||||
} catch (e) {
|
||||
console.error(`Playwright: Connection dispatch error`);
|
||||
console.error(e);
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
|
@ -150,8 +152,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
}
|
||||
}
|
||||
ws.addEventListener('open', async () => {
|
||||
const prematureCloseListener = (event: { reason: string }) => {
|
||||
reject(new Error('Server disconnected: ' + event.reason));
|
||||
const prematureCloseListener = (event: { code: number, reason: string }) => {
|
||||
reject(new Error(`WebSocket server disconnected (${event.code}) ${event.reason}`));
|
||||
};
|
||||
ws.addEventListener('close', prematureCloseListener);
|
||||
const playwright = await connection.waitForObjectWithKnownName('Playwright') as Playwright;
|
||||
|
|
@ -182,6 +184,16 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
ws.removeEventListener('close', closeListener);
|
||||
ws.close();
|
||||
});
|
||||
if (params._forwardPorts) {
|
||||
try {
|
||||
await playwright._channel.enablePortForwarding({
|
||||
ports: params._forwardPorts,
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fulfill(browser);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import { debugLogger } from '../utils/debugLogger';
|
|||
import { SelectorsOwner } from './selectors';
|
||||
import { isUnderTest } from '../utils/utils';
|
||||
import { Android, AndroidSocket, AndroidDevice } from './android';
|
||||
import { SocksSocket } from './socksSocket';
|
||||
import { captureStackTrace } from '../utils/stackTrace';
|
||||
import { Artifact } from './artifact';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
class Root extends ChannelOwner<channels.Channel, {}> {
|
||||
constructor(connection: Connection) {
|
||||
|
|
@ -45,7 +47,7 @@ class Root extends ChannelOwner<channels.Channel, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
export class Connection {
|
||||
export class Connection extends EventEmitter {
|
||||
readonly _objects = new Map<string, ChannelOwner>();
|
||||
private _waitingForObject = new Map<string, any>();
|
||||
onmessage = (message: object): void => {};
|
||||
|
|
@ -56,6 +58,7 @@ export class Connection {
|
|||
private _onClose?: () => void;
|
||||
|
||||
constructor(onClose?: () => void) {
|
||||
super();
|
||||
this._rootObject = new Root(this);
|
||||
this._onClose = onClose;
|
||||
}
|
||||
|
|
@ -135,6 +138,7 @@ export class Connection {
|
|||
for (const callback of this._callbacks.values())
|
||||
callback.reject(new Error(errorMessage));
|
||||
this._callbacks.clear();
|
||||
this.emit('disconnect');
|
||||
}
|
||||
|
||||
isDisconnected() {
|
||||
|
|
@ -239,6 +243,9 @@ export class Connection {
|
|||
case 'Worker':
|
||||
result = new Worker(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'SocksSocket':
|
||||
result = new SocksSocket(parent, type, guid, initializer);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Missing type ' + type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { Electron } from './electron';
|
|||
import { TimeoutError } from '../utils/errors';
|
||||
import { Size } from './types';
|
||||
import { Android } from './android';
|
||||
import { SocksSocket } from './socksSocket';
|
||||
|
||||
type DeviceDescriptor = {
|
||||
userAgent: string,
|
||||
|
|
@ -59,6 +60,8 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
|||
|
||||
this._selectorsOwner = SelectorsOwner.from(initializer.selectors);
|
||||
this.selectors._addChannel(this._selectorsOwner);
|
||||
|
||||
this._channel.on('incomingSocksSocket', ({socket}) => SocksSocket.from(socket));
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
|
|
|
|||
66
src/client/socksSocket.ts
Normal file
66
src/client/socksSocket.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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 from 'net';
|
||||
|
||||
import * as channels from '../protocol/channels';
|
||||
import { assert, isLocalIpAddress, isUnderTest } from '../utils/utils';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
||||
export class SocksSocket extends ChannelOwner<channels.SocksSocketChannel, channels.SocksSocketInitializer> {
|
||||
private _socket: net.Socket;
|
||||
static from(socket: channels.SocksSocketChannel): SocksSocket {
|
||||
return (socket as any)._object;
|
||||
}
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.SocksSocketInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._connection.on('disconnect', () => this._socket.end());
|
||||
|
||||
if (isUnderTest() && process.env.PW_TEST_PROXY_TARGET)
|
||||
this._initializer.dstPort = Number(process.env.PW_TEST_PROXY_TARGET);
|
||||
assert(isLocalIpAddress(this._initializer.dstAddr));
|
||||
|
||||
this._socket = net.createConnection(this._initializer.dstPort, this._initializer.dstAddr);
|
||||
this._socket.on('error', (err: Error) => this._channel.error({error: String(err)}));
|
||||
this._socket.on('connect', () => {
|
||||
this.connected().catch(() => {});
|
||||
this._socket.on('data', data => this.write(data).catch(() => {}));
|
||||
});
|
||||
this._socket.on('close', () => {
|
||||
this.end().catch(() => {});
|
||||
});
|
||||
|
||||
this._channel.on('data', ({ data }) => {
|
||||
if (!this._socket.writable)
|
||||
return;
|
||||
this._socket.write(Buffer.from(data, 'base64'));
|
||||
});
|
||||
this._channel.on('close', () => this._socket.end());
|
||||
}
|
||||
|
||||
async write(data: Buffer): Promise<void> {
|
||||
await this._channel.write({ data: data.toString('base64') });
|
||||
}
|
||||
|
||||
async end(): Promise<void> {
|
||||
await this._channel.end();
|
||||
}
|
||||
|
||||
async connected(): Promise<void> {
|
||||
await this._channel.connected();
|
||||
}
|
||||
}
|
||||
|
|
@ -71,11 +71,13 @@ export type LaunchPersistentContextOptions = Omit<LaunchOptionsBase & BrowserCon
|
|||
export type ConnectOptions = {
|
||||
wsEndpoint: string,
|
||||
headers?: { [key: string]: string; };
|
||||
_forwardPorts?: number[];
|
||||
slowMo?: number,
|
||||
timeout?: number,
|
||||
logger?: Logger,
|
||||
};
|
||||
export type LaunchServerOptions = {
|
||||
_acceptForwardedPorts?: boolean,
|
||||
channel?: channels.BrowserTypeLaunchOptions['channel'],
|
||||
executablePath?: string,
|
||||
args?: string[],
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import { Dispatcher, DispatcherScope } from './dispatcher';
|
|||
import { ElectronDispatcher } from './electronDispatcher';
|
||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||
import * as types from '../server/types';
|
||||
import { assert } from '../utils/utils';
|
||||
|
||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightInitializer> implements channels.PlaywrightChannel {
|
||||
constructor(scope: DispatcherScope, playwright: Playwright, customSelectors?: channels.SelectorsChannel, preLaunchedBrowser?: channels.BrowserChannel) {
|
||||
private _portForwardingCallback: ((ports: number[]) => void) | undefined;
|
||||
constructor(scope: DispatcherScope, playwright: Playwright, customSelectors?: channels.SelectorsChannel, preLaunchedBrowser?: channels.BrowserChannel, portForwardingCallback?: (ports: number[]) => void) {
|
||||
const descriptors = require('../server/deviceDescriptors') as types.Devices;
|
||||
const deviceDescriptors = Object.entries(descriptors)
|
||||
.map(([name, descriptor]) => ({ name, descriptor }));
|
||||
|
|
@ -38,5 +40,11 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
|||
selectors: customSelectors || new SelectorsDispatcher(scope, playwright.selectors),
|
||||
preLaunchedBrowser,
|
||||
}, false);
|
||||
this._portForwardingCallback = portForwardingCallback;
|
||||
}
|
||||
|
||||
async enablePortForwarding(params: channels.PlaywrightEnablePortForwardingParams): Promise<void> {
|
||||
assert(this._portForwardingCallback, 'Port forwarding is only supported when using connect()');
|
||||
this._portForwardingCallback(params.ports);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
src/dispatchers/socksSocketDispatcher.ts
Normal file
47
src/dispatchers/socksSocketDispatcher.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 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 { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { SocksInterceptedSocketHandler } from '../server/socksServer';
|
||||
|
||||
export class SocksSocketDispatcher extends Dispatcher<SocksInterceptedSocketHandler, channels.SocksSocketInitializer> implements channels.SocksSocketChannel {
|
||||
constructor(scope: DispatcherScope, socket: SocksInterceptedSocketHandler) {
|
||||
super(scope, socket, 'SocksSocket', {
|
||||
dstAddr: socket.dstAddr,
|
||||
dstPort: socket.dstPort
|
||||
}, true);
|
||||
socket.on('data', (data: Buffer) => this._dispatchEvent('data', { data: data.toString('base64') }));
|
||||
socket.on('close', () => {
|
||||
this._dispatchEvent('close');
|
||||
this._dispose();
|
||||
});
|
||||
}
|
||||
async connected(): Promise<void> {
|
||||
this._object.connected();
|
||||
}
|
||||
async error(params: channels.SocksSocketErrorParams): Promise<void> {
|
||||
this._object.error(params.error);
|
||||
}
|
||||
|
||||
async write(params: channels.SocksSocketWriteParams): Promise<void> {
|
||||
this._object.write(Buffer.from(params.data, 'base64'));
|
||||
}
|
||||
|
||||
async end(): Promise<void> {
|
||||
this._object.end();
|
||||
}
|
||||
}
|
||||
|
|
@ -179,7 +179,19 @@ export type PlaywrightInitializer = {
|
|||
preLaunchedBrowser?: BrowserChannel,
|
||||
};
|
||||
export interface PlaywrightChannel extends Channel {
|
||||
on(event: 'incomingSocksSocket', callback: (params: PlaywrightIncomingSocksSocketEvent) => void): this;
|
||||
enablePortForwarding(params: PlaywrightEnablePortForwardingParams, metadata?: Metadata): Promise<PlaywrightEnablePortForwardingResult>;
|
||||
}
|
||||
export type PlaywrightIncomingSocksSocketEvent = {
|
||||
socket: SocksSocketChannel,
|
||||
};
|
||||
export type PlaywrightEnablePortForwardingParams = {
|
||||
ports: number[],
|
||||
};
|
||||
export type PlaywrightEnablePortForwardingOptions = {
|
||||
|
||||
};
|
||||
export type PlaywrightEnablePortForwardingResult = void;
|
||||
|
||||
// ----------- Selectors -----------
|
||||
export type SelectorsInitializer = {};
|
||||
|
|
@ -3113,3 +3125,41 @@ export type AndroidElementInfo = {
|
|||
scrollable: boolean,
|
||||
selected: boolean,
|
||||
};
|
||||
|
||||
// ----------- SocksSocket -----------
|
||||
export type SocksSocketInitializer = {
|
||||
dstAddr: string,
|
||||
dstPort: number,
|
||||
};
|
||||
export interface SocksSocketChannel extends Channel {
|
||||
on(event: 'data', callback: (params: SocksSocketDataEvent) => void): this;
|
||||
on(event: 'close', callback: (params: SocksSocketCloseEvent) => void): this;
|
||||
write(params: SocksSocketWriteParams, metadata?: Metadata): Promise<SocksSocketWriteResult>;
|
||||
error(params: SocksSocketErrorParams, metadata?: Metadata): Promise<SocksSocketErrorResult>;
|
||||
connected(params?: SocksSocketConnectedParams, metadata?: Metadata): Promise<SocksSocketConnectedResult>;
|
||||
end(params?: SocksSocketEndParams, metadata?: Metadata): Promise<SocksSocketEndResult>;
|
||||
}
|
||||
export type SocksSocketDataEvent = {
|
||||
data: Binary,
|
||||
};
|
||||
export type SocksSocketCloseEvent = {};
|
||||
export type SocksSocketWriteParams = {
|
||||
data: Binary,
|
||||
};
|
||||
export type SocksSocketWriteOptions = {
|
||||
|
||||
};
|
||||
export type SocksSocketWriteResult = void;
|
||||
export type SocksSocketErrorParams = {
|
||||
error: string,
|
||||
};
|
||||
export type SocksSocketErrorOptions = {
|
||||
|
||||
};
|
||||
export type SocksSocketErrorResult = void;
|
||||
export type SocksSocketConnectedParams = {};
|
||||
export type SocksSocketConnectedOptions = {};
|
||||
export type SocksSocketConnectedResult = void;
|
||||
export type SocksSocketEndParams = {};
|
||||
export type SocksSocketEndOptions = {};
|
||||
export type SocksSocketEndResult = void;
|
||||
|
|
|
|||
|
|
@ -377,6 +377,20 @@ Playwright:
|
|||
# Only present when connecting remotely via BrowserType.connect() method.
|
||||
preLaunchedBrowser: Browser?
|
||||
|
||||
commands:
|
||||
|
||||
enablePortForwarding:
|
||||
parameters:
|
||||
ports:
|
||||
type: array
|
||||
items: number
|
||||
|
||||
events:
|
||||
|
||||
incomingSocksSocket:
|
||||
parameters:
|
||||
socket: SocksSocket
|
||||
|
||||
|
||||
Selectors:
|
||||
type: interface
|
||||
|
|
@ -2523,3 +2537,29 @@ AndroidElementInfo:
|
|||
longClickable: boolean
|
||||
scrollable: boolean
|
||||
selected: boolean
|
||||
|
||||
SocksSocket:
|
||||
type: interface
|
||||
|
||||
initializer:
|
||||
dstAddr: string
|
||||
dstPort: number
|
||||
|
||||
commands:
|
||||
write:
|
||||
parameters:
|
||||
data: binary
|
||||
|
||||
error:
|
||||
parameters:
|
||||
error: string
|
||||
|
||||
connected:
|
||||
|
||||
end:
|
||||
|
||||
events:
|
||||
data:
|
||||
parameters:
|
||||
data: binary
|
||||
close:
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
})),
|
||||
value: tOptional(tType('SerializedValue')),
|
||||
});
|
||||
scheme.PlaywrightEnablePortForwardingParams = tObject({
|
||||
ports: tArray(tNumber),
|
||||
});
|
||||
scheme.SelectorsRegisterParams = tObject({
|
||||
name: tString,
|
||||
source: tString,
|
||||
|
|
@ -1221,6 +1224,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scrollable: tBoolean,
|
||||
selected: tBoolean,
|
||||
});
|
||||
scheme.SocksSocketWriteParams = tObject({
|
||||
data: tBinary,
|
||||
});
|
||||
scheme.SocksSocketErrorParams = tObject({
|
||||
error: tString,
|
||||
});
|
||||
scheme.SocksSocketConnectedParams = tOptional(tObject({}));
|
||||
scheme.SocksSocketEndParams = tOptional(tObject({}));
|
||||
|
||||
return scheme;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ import { CallMetadata } from '../instrumentation';
|
|||
import { findChromiumChannel } from './findChromiumChannel';
|
||||
import http from 'http';
|
||||
|
||||
type LaunchServerOptions = {
|
||||
_acceptForwardedPorts?: boolean,
|
||||
};
|
||||
|
||||
export class Chromium extends BrowserType {
|
||||
private _devtools: CRDevTools | undefined;
|
||||
|
||||
|
|
@ -119,7 +123,7 @@ export class Chromium extends BrowserType {
|
|||
transport.send(message);
|
||||
}
|
||||
|
||||
_defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
_defaultArgs(options: types.LaunchOptions & LaunchServerOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const { args = [], proxy } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
|
|
@ -155,10 +159,14 @@ export class Chromium extends BrowserType {
|
|||
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
|
||||
}
|
||||
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||
if (proxy.bypass) {
|
||||
const patterns = proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t);
|
||||
chromeArguments.push(`--proxy-bypass-list=${patterns.join(';')}`);
|
||||
}
|
||||
const proxyBypassRules = [];
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
|
||||
if (options._acceptForwardedPorts)
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxy.bypass)
|
||||
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||
if (proxyBypassRules.length > 0)
|
||||
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
|
||||
}
|
||||
chromeArguments.push(...args);
|
||||
if (isPersistent)
|
||||
|
|
|
|||
300
src/server/socksServer.ts
Normal file
300
src/server/socksServer.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
/**
|
||||
* 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 from 'net';
|
||||
|
||||
import { assert } from '../utils/utils';
|
||||
import { SdkObject } from './instrumentation';
|
||||
|
||||
export type SocksConnectionInfo = {
|
||||
srcAddr: string,
|
||||
srcPort: number,
|
||||
dstAddr: string;
|
||||
dstPort: number;
|
||||
};
|
||||
|
||||
enum ConnectionPhases {
|
||||
VERSION = 0,
|
||||
NMETHODS,
|
||||
METHODS,
|
||||
REQ_CMD,
|
||||
REQ_RSV,
|
||||
REQ_ATYP,
|
||||
REQ_DSTADDR,
|
||||
REQ_DSTADDR_VARLEN,
|
||||
REQ_DSTPORT,
|
||||
DONE,
|
||||
}
|
||||
|
||||
enum SOCKS_AUTH_METHOD {
|
||||
NO_AUTH = 0
|
||||
}
|
||||
|
||||
enum SOCKS_CMD {
|
||||
CONNECT = 0x01,
|
||||
BIND = 0x02,
|
||||
UDP = 0x03
|
||||
}
|
||||
|
||||
enum SOCKS_ATYP {
|
||||
IPv4 = 0x01,
|
||||
NAME = 0x03,
|
||||
IPv6 = 0x04
|
||||
}
|
||||
|
||||
enum SOCKS_REPLY {
|
||||
SUCCESS = 0x00,
|
||||
}
|
||||
|
||||
const SOCKS_VERSION = 0x5;
|
||||
|
||||
const BUF_REP_INTR_SUCCESS = Buffer.from([
|
||||
0x05,
|
||||
SOCKS_REPLY.SUCCESS,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc1928
|
||||
*/
|
||||
class SocksV5ServerParser {
|
||||
private _dstAddrp: number = 0;
|
||||
private _dstPort?: number;
|
||||
private _socket: net.Socket;
|
||||
private _readyResolve!: (value?: unknown) => void;
|
||||
private _ready: Promise<unknown>;
|
||||
private _info: SocksConnectionInfo;
|
||||
private _phase: ConnectionPhases = ConnectionPhases.VERSION;
|
||||
private _authMethods?: Buffer;
|
||||
private _dstAddr?: Buffer;
|
||||
private _addressType: any;
|
||||
private _methodsp: number = 0;
|
||||
constructor(socket: net.Socket) {
|
||||
this._socket = socket;
|
||||
this._info = { srcAddr: socket.remoteAddress!, srcPort: socket.remotePort!, dstAddr: '', dstPort: 0 };
|
||||
this._ready = new Promise(resolve => this._readyResolve = resolve);
|
||||
socket.on('data', this._onData.bind(this));
|
||||
socket.on('error', () => {});
|
||||
}
|
||||
private _onData(chunk: Buffer) {
|
||||
const socket = this._socket;
|
||||
let i = 0;
|
||||
const readByte = () => chunk[i++];
|
||||
while (i < chunk.length && this._phase !== ConnectionPhases.DONE) {
|
||||
switch (this._phase) {
|
||||
case ConnectionPhases.VERSION:
|
||||
assert(readByte() === SOCKS_VERSION);
|
||||
this._phase = ConnectionPhases.NMETHODS;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.NMETHODS:
|
||||
this._authMethods = Buffer.alloc(readByte());
|
||||
this._phase = ConnectionPhases.METHODS;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.METHODS: {
|
||||
assert(this._authMethods);
|
||||
chunk.copy(this._authMethods, 0, i, i + chunk.length);
|
||||
assert(this._authMethods.includes(SOCKS_AUTH_METHOD.NO_AUTH));
|
||||
const left = this._authMethods.length - this._methodsp;
|
||||
const chunkLeft = chunk.length - i;
|
||||
const minLen = (left < chunkLeft ? left : chunkLeft);
|
||||
chunk.copy(this._authMethods, this._methodsp, i, i + minLen);
|
||||
this._methodsp += minLen;
|
||||
i += minLen;
|
||||
assert(this._methodsp === this._authMethods.length);
|
||||
if (i < chunk.length)
|
||||
this._socket.unshift(chunk.slice(i));
|
||||
this._authWithoutPassword(socket);
|
||||
this._phase = ConnectionPhases.REQ_CMD;
|
||||
break;
|
||||
}
|
||||
|
||||
case ConnectionPhases.REQ_CMD:
|
||||
assert(readByte() === SOCKS_VERSION);
|
||||
const cmd: SOCKS_CMD = readByte();
|
||||
assert(cmd === SOCKS_CMD.CONNECT);
|
||||
this._phase = ConnectionPhases.REQ_RSV;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.REQ_RSV:
|
||||
readByte();
|
||||
this._phase = ConnectionPhases.REQ_ATYP;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.REQ_ATYP:
|
||||
this._phase = ConnectionPhases.REQ_DSTADDR;
|
||||
this._addressType = readByte();
|
||||
assert(this._addressType in SOCKS_ATYP);
|
||||
if (this._addressType === SOCKS_ATYP.IPv4)
|
||||
this._dstAddr = Buffer.alloc(4);
|
||||
else if (this._addressType === SOCKS_ATYP.IPv6)
|
||||
this._dstAddr = Buffer.alloc(16);
|
||||
else if (this._addressType === SOCKS_ATYP.NAME)
|
||||
this._phase = ConnectionPhases.REQ_DSTADDR_VARLEN;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.REQ_DSTADDR: {
|
||||
assert(this._dstAddr);
|
||||
const left = this._dstAddr.length - this._dstAddrp;
|
||||
const chunkLeft = chunk.length - i;
|
||||
const minLen = (left < chunkLeft ? left : chunkLeft);
|
||||
chunk.copy(this._dstAddr, this._dstAddrp, i, i + minLen);
|
||||
this._dstAddrp += minLen;
|
||||
i += minLen;
|
||||
if (this._dstAddrp === this._dstAddr.length)
|
||||
this._phase = ConnectionPhases.REQ_DSTPORT;
|
||||
break;
|
||||
}
|
||||
|
||||
case ConnectionPhases.REQ_DSTADDR_VARLEN:
|
||||
this._dstAddr = Buffer.alloc(readByte());
|
||||
this._phase = ConnectionPhases.REQ_DSTADDR;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.REQ_DSTPORT:
|
||||
assert(this._dstAddr);
|
||||
if (this._dstPort === undefined) {
|
||||
this._dstPort = readByte();
|
||||
break;
|
||||
}
|
||||
this._dstPort <<= 8;
|
||||
this._dstPort += readByte();
|
||||
|
||||
this._socket.removeListener('data', this._onData);
|
||||
if (i < chunk.length)
|
||||
this._socket.unshift(chunk.slice(i));
|
||||
|
||||
if (this._addressType === SOCKS_ATYP.IPv4) {
|
||||
this._info.dstAddr = [...this._dstAddr].join('.');
|
||||
} else if (this._addressType === SOCKS_ATYP.IPv6) {
|
||||
let ipv6str = '';
|
||||
const addr = this._dstAddr;
|
||||
for (let b = 0; b < 16; ++b) {
|
||||
if (b % 2 === 0 && b > 0)
|
||||
ipv6str += ':';
|
||||
ipv6str += (addr[b] < 16 ? '0' : '') + addr[b].toString(16);
|
||||
}
|
||||
this._info.dstAddr = ipv6str;
|
||||
} else {
|
||||
this._info.dstAddr = this._dstAddr.toString();
|
||||
}
|
||||
this._info.dstPort = this._dstPort;
|
||||
this._phase = ConnectionPhases.DONE;
|
||||
this._readyResolve();
|
||||
return;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _authWithoutPassword(socket: net.Socket) {
|
||||
socket.write(Buffer.from([0x05, 0x00]));
|
||||
}
|
||||
|
||||
async ready(): Promise<{ info: SocksConnectionInfo, forward: () => void, intercept: (parent: SdkObject) => SocksInterceptedSocketHandler }> {
|
||||
await this._ready;
|
||||
return {
|
||||
info: this._info,
|
||||
forward: () => {
|
||||
const dstSocket = new net.Socket();
|
||||
this._socket.on('close', () => dstSocket.end());
|
||||
this._socket.on('end', () => dstSocket.end());
|
||||
dstSocket.setKeepAlive(false);
|
||||
dstSocket.on('error', (err: NodeJS.ErrnoException) => writeSocksSocketError(this._socket, String(err)));
|
||||
dstSocket.on('connect', () => {
|
||||
this._socket.write(BUF_REP_INTR_SUCCESS);
|
||||
this._socket.pipe(dstSocket).pipe(this._socket);
|
||||
this._socket.resume();
|
||||
}).connect(this._info.dstPort, this._info.dstAddr);
|
||||
},
|
||||
intercept: (parent: SdkObject): SocksInterceptedSocketHandler => {
|
||||
return new SocksInterceptedSocketHandler(parent, this._socket, this._info.dstAddr, this._info.dstPort);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class SocksInterceptedSocketHandler extends SdkObject {
|
||||
socket: net.Socket;
|
||||
dstAddr: string;
|
||||
dstPort: number;
|
||||
constructor(parent: SdkObject, socket: net.Socket, dstAddr: string, dstPort: number) {
|
||||
super(parent, 'SocksSocket');
|
||||
this.socket = socket;
|
||||
this.dstAddr = dstAddr;
|
||||
this.dstPort = dstPort;
|
||||
socket.on('data', data => this.emit('data', data));
|
||||
socket.on('close', data => this.emit('close', data));
|
||||
}
|
||||
connected() {
|
||||
this.socket.write(BUF_REP_INTR_SUCCESS);
|
||||
this.socket.resume();
|
||||
}
|
||||
error(error: string) {
|
||||
this.socket.resume();
|
||||
writeSocksSocketError(this.socket, error);
|
||||
}
|
||||
write(data: Buffer) {
|
||||
this.socket.write(data);
|
||||
}
|
||||
end() {
|
||||
this.socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
function writeSocksSocketError(socket: net.Socket, error: string) {
|
||||
if (!socket.writable)
|
||||
return;
|
||||
socket.write(BUF_REP_INTR_SUCCESS);
|
||||
|
||||
const body = `Connection error: ${error}`;
|
||||
socket.end([
|
||||
'HTTP/1.1 502 OK',
|
||||
'Connection: close',
|
||||
'Content-Type: text/plain',
|
||||
'Content-Length: ' + Buffer.byteLength(body),
|
||||
'',
|
||||
body
|
||||
].join('\r\n'));
|
||||
}
|
||||
|
||||
type IncomingProxyRequestHandler = (info: SocksConnectionInfo, forward: () => void, intercept: (parent: SdkObject) => SocksInterceptedSocketHandler) => void;
|
||||
|
||||
export class SocksProxyServer {
|
||||
public server: net.Server;
|
||||
constructor(incomingMessageHandler: IncomingProxyRequestHandler) {
|
||||
this.server = net.createServer(this._handleConnection.bind(this, incomingMessageHandler));
|
||||
}
|
||||
|
||||
public listen(port: number, host?: string) {
|
||||
this.server.listen(port, host);
|
||||
}
|
||||
|
||||
async _handleConnection(incomingMessageHandler: IncomingProxyRequestHandler, socket: net.Socket) {
|
||||
const parser = new SocksV5ServerParser(socket);
|
||||
const { info, forward, intercept } = await parser.ready();
|
||||
incomingMessageHandler(info, forward, intercept);
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
83
src/server/socksSocket.ts
Normal file
83
src/server/socksSocket.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* 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 from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { SdkObject } from './instrumentation';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { isLocalIpAddress } from '../utils/utils';
|
||||
import { SocksProxyServer, SocksConnectionInfo, SocksInterceptedSocketHandler } from './socksServer';
|
||||
import { LaunchOptions } from './types';
|
||||
|
||||
export class BrowserServerPortForwardingServer extends EventEmitter {
|
||||
enabled: boolean;
|
||||
private _forwardPorts: number[] = [];
|
||||
private _parent: SdkObject;
|
||||
private _server: SocksProxyServer;
|
||||
constructor(parent: SdkObject, enabled: boolean) {
|
||||
super();
|
||||
this.setMaxListeners(0);
|
||||
this.enabled = enabled;
|
||||
this._parent = parent;
|
||||
this._server = new SocksProxyServer(this._handler.bind(this));
|
||||
if (enabled) {
|
||||
this._server.listen(0);
|
||||
debugLogger.log('proxy', `initialized server on port ${this._port()})`);
|
||||
}
|
||||
}
|
||||
|
||||
private _port(): number {
|
||||
if (!this.enabled)
|
||||
return 0;
|
||||
return (this._server.server.address() as net.AddressInfo).port;
|
||||
}
|
||||
|
||||
public browserLaunchOptions(): LaunchOptions | undefined {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
return {
|
||||
proxy: {
|
||||
server: `socks5://127.0.0.1:${this._port()}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handler(info: SocksConnectionInfo, forward: () => void, intercept: (parent: SdkObject) => SocksInterceptedSocketHandler): void {
|
||||
const shouldProxyRequestToClient = isLocalIpAddress(info.dstAddr) && this._forwardPorts.includes(info.dstPort);
|
||||
debugLogger.log('proxy', `incoming connection from ${info.srcAddr}:${info.srcPort} to ${info.dstAddr}:${info.dstPort} shouldProxyRequestToClient=${shouldProxyRequestToClient}`);
|
||||
if (!shouldProxyRequestToClient) {
|
||||
forward();
|
||||
return;
|
||||
}
|
||||
const socket = intercept(this._parent);
|
||||
this.emit('incomingSocksSocket', socket);
|
||||
}
|
||||
|
||||
public enablePortForwarding(ports: number[]): void {
|
||||
if (!this.enabled)
|
||||
throw new Error(`Port forwarding needs to be enabled when launching the server via BrowserType.launchServer.`);
|
||||
debugLogger.log('proxy', `enable port forwarding on ports: ${ports}`);
|
||||
this._forwardPorts = ports;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
debugLogger.log('proxy', 'stopping server');
|
||||
this._server.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ const debugLoggerColorMap = {
|
|||
'protocol': 34, // green
|
||||
'install': 34, // green
|
||||
'browser': 0, // reset
|
||||
'proxy': 92, // purple
|
||||
'error': 160, // red,
|
||||
'channel:command': 33, // blue
|
||||
'channel:response': 202, // orange
|
||||
|
|
|
|||
|
|
@ -177,3 +177,7 @@ export function canAccessFile(file: string) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isLocalIpAddress(ipAdress: string): boolean {
|
||||
return ['localhost', '127.0.0.1', '::ffff:127.0.0.1', '::1'].includes(ipAdress);
|
||||
}
|
||||
|
|
|
|||
162
tests/portForwardingServer.spec.ts
Normal file
162
tests/portForwardingServer.spec.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* 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 http from 'http';
|
||||
|
||||
import { contextTest as it, expect } from './config/browserTest';
|
||||
import type { LaunchOptions, ConnectOptions } from '../index';
|
||||
|
||||
it.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
let targetTestServer: http.Server;
|
||||
let port!: number;
|
||||
it.beforeAll(async ({}, test) => {
|
||||
port = 30_000 + test.workerIndex * 4;
|
||||
targetTestServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
res.end('<html><body>from-retargeted-server</body></html>');
|
||||
}).listen(port);
|
||||
});
|
||||
|
||||
it.beforeEach(() => {
|
||||
delete process.env.PW_TEST_PROXY_TARGET;
|
||||
});
|
||||
|
||||
it.afterAll(() => {
|
||||
targetTestServer.close();
|
||||
});
|
||||
|
||||
it('should forward non-forwarded requests', async ({ browserType, browserOptions, server }, workerInfo) => {
|
||||
process.env.PW_TEST_PROXY_TARGET = port.toString();
|
||||
let reachedOriginalTarget = false;
|
||||
server.setRoute('/foo.html', async (req, res) => {
|
||||
reachedOriginalTarget = true;
|
||||
res.end('<html><body>original-target</body></html>');
|
||||
});
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: true
|
||||
} as LaunchOptions);
|
||||
const browser = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: []
|
||||
} as ConnectOptions);
|
||||
const page = await browser.newPage();
|
||||
await page.goto(server.PREFIX + '/foo.html');
|
||||
expect(await page.content()).toContain('original-target');
|
||||
expect(reachedOriginalTarget).toBe(true);
|
||||
await browserServer.close();
|
||||
});
|
||||
|
||||
it('should proxy local requests', async ({ browserType, browserOptions, server }, workerInfo) => {
|
||||
process.env.PW_TEST_PROXY_TARGET = port.toString();
|
||||
let reachedOriginalTarget = false;
|
||||
server.setRoute('/foo.html', async (req, res) => {
|
||||
reachedOriginalTarget = true;
|
||||
res.end('<html><body></body></html>');
|
||||
});
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: true
|
||||
} as LaunchOptions);
|
||||
const browser = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: [examplePort]
|
||||
} as ConnectOptions);
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://localhost:${examplePort}/foo.html`);
|
||||
expect(await page.content()).toContain('from-retargeted-server');
|
||||
expect(reachedOriginalTarget).toBe(false);
|
||||
await browserServer.close();
|
||||
});
|
||||
|
||||
it('should lead to the error page for forwarded requests when the connection is refused', async ({ browserType, browserOptions, browserName, isWindows}, workerInfo) => {
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: true
|
||||
} as LaunchOptions);
|
||||
const browser = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: [examplePort]
|
||||
} as ConnectOptions);
|
||||
const page = await browser.newPage();
|
||||
const response = await page.goto(`http://localhost:${examplePort}`);
|
||||
expect(response.status()).toBe(502);
|
||||
await page.waitForSelector('text=Connection error');
|
||||
await browserServer.close();
|
||||
});
|
||||
|
||||
it('should lead to the error page for non-forwarded requests when the connection is refused', async ({ browserName, browserType, browserOptions, isWindows}, workerInfo) => {
|
||||
process.env.PW_TEST_PROXY_TARGET = '50001';
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: true
|
||||
} as LaunchOptions);
|
||||
const browser = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: []
|
||||
} as ConnectOptions);
|
||||
const page = await browser.newPage();
|
||||
const response = await page.goto(`http://localhost:44123/non-existing-url`);
|
||||
expect(response.status()).toBe(502);
|
||||
await page.waitForSelector('text=Connection error');
|
||||
|
||||
await browserServer.close();
|
||||
});
|
||||
|
||||
it('should not allow connecting a second client when _acceptForwardedPorts is used', async ({ browserType, browserOptions }, workerInfo) => {
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: true
|
||||
} as LaunchOptions);
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
|
||||
const browser1 = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: [examplePort]
|
||||
} as ConnectOptions);
|
||||
await expect(browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: [examplePort]
|
||||
} as ConnectOptions)).rejects.toThrowError('browserType.connect: WebSocket server disconnected (1005)');
|
||||
await browser1.close();
|
||||
const browser2 = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: [examplePort]
|
||||
} as ConnectOptions);
|
||||
await browser2.close();
|
||||
|
||||
await browserServer.close();
|
||||
});
|
||||
|
||||
it('should should not allow to connect when the server does not allow port-forwarding', async ({ browserType, browserOptions }, workerInfo) => {
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: false
|
||||
} as LaunchOptions);
|
||||
|
||||
await expect(browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: []
|
||||
} as ConnectOptions)).rejects.toThrowError('browserType.connect: Port forwarding needs to be enabled when launching the server via BrowserType.launchServer.');
|
||||
await expect(browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: [1234]
|
||||
} as ConnectOptions)).rejects.toThrowError('browserType.connect: Port forwarding needs to be enabled when launching the server via BrowserType.launchServer.');
|
||||
|
||||
await browserServer.close();
|
||||
});
|
||||
Loading…
Reference in a new issue