chore: reimplement socks to be readable (#8315)
This commit is contained in:
parent
96a9a26f9f
commit
44887c237d
|
|
@ -44,8 +44,6 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
|
||||
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
|
||||
const playwright = createPlaywright();
|
||||
if (options._acceptForwardedPorts)
|
||||
await playwright._enablePortForwarding();
|
||||
// 1. Pre-launch the browser
|
||||
const browser = await playwright[this._browserName].launch(internalCallMetadata(), {
|
||||
...options,
|
||||
|
|
@ -57,10 +55,8 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
// 2. Start the server
|
||||
const delegate: PlaywrightServerDelegate = {
|
||||
path: '/' + createGuid(),
|
||||
allowMultipleClients: options._acceptForwardedPorts ? false : true,
|
||||
onClose: () => {
|
||||
playwright._disablePortForwarding();
|
||||
},
|
||||
allowMultipleClients: true,
|
||||
onClose: () => {},
|
||||
onConnect: this._onConnect.bind(this, playwright, browser),
|
||||
};
|
||||
const server = new PlaywrightServer(delegate);
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ if (!process.env.PW_CLI_TARGET_LANG) {
|
|||
if (process.argv[2] === 'run-driver')
|
||||
runDriver();
|
||||
else if (process.argv[2] === 'run-server')
|
||||
runServer(process.argv[3] ? +process.argv[3] : undefined, process.argv[4]).catch(logErrorAndExit);
|
||||
runServer(process.argv[3] ? +process.argv[3] : undefined).catch(logErrorAndExit);
|
||||
else if (process.argv[2] === 'print-api-json')
|
||||
printApiJson();
|
||||
else if (process.argv[2] === 'launch-server')
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { LaunchServerOptions } from '../client/types';
|
|||
import { DispatcherConnection, Root } from '../dispatchers/dispatcher';
|
||||
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
|
||||
import { Transport } from '../protocol/transport';
|
||||
import { PlaywrightServer, PlaywrightServerOptions } from '../remote/playwrightServer';
|
||||
import { PlaywrightServer } from '../remote/playwrightServer';
|
||||
import { createPlaywright } from '../server/playwright';
|
||||
import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||
|
||||
|
|
@ -52,11 +52,8 @@ export function runDriver() {
|
|||
};
|
||||
}
|
||||
|
||||
export async function runServer(port: number | undefined, configFile?: string) {
|
||||
let options: PlaywrightServerOptions = {};
|
||||
if (configFile)
|
||||
options = JSON.parse(fs.readFileSync(configFile).toString());
|
||||
const server = await PlaywrightServer.startDefault(options);
|
||||
export async function runServer(port: number | undefined) {
|
||||
const server = await PlaywrightServer.startDefault();
|
||||
const wsEndpoint = await server.listen(port);
|
||||
process.on('exit', () => server.close().catch(console.error));
|
||||
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
||||
|
|
|
|||
|
|
@ -210,14 +210,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||
ws.removeEventListener('close', closeListener);
|
||||
ws.close();
|
||||
});
|
||||
if (params._forwardPorts) {
|
||||
try {
|
||||
await playwright._enablePortForwarding(params._forwardPorts);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fulfill(browser);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import { Stream } from './stream';
|
|||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { SelectorsOwner } from './selectors';
|
||||
import { Android, AndroidSocket, AndroidDevice } from './android';
|
||||
import { SocksSocket } from './socksSocket';
|
||||
import { ParsedStackTrace } from '../utils/stackTrace';
|
||||
import { Artifact } from './artifact';
|
||||
import { EventEmitter } from 'events';
|
||||
|
|
@ -248,9 +247,6 @@ export class Connection extends EventEmitter {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import dns from 'dns';
|
||||
import net from 'net';
|
||||
import util from 'util';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { TimeoutError } from '../utils/errors';
|
||||
import { createSocket } from '../utils/netUtils';
|
||||
import { Android } from './android';
|
||||
import { BrowserType } from './browserType';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Selectors, SelectorsOwner, sharedSelectors } from './selectors';
|
||||
import { Electron } from './electron';
|
||||
import { TimeoutError } from '../utils/errors';
|
||||
import { Selectors, SelectorsOwner, sharedSelectors } from './selectors';
|
||||
import { Size } from './types';
|
||||
import { Android } from './android';
|
||||
import { SocksSocket } from './socksSocket';
|
||||
const dnsLookupAsync = util.promisify(dns.lookup);
|
||||
|
||||
type DeviceDescriptor = {
|
||||
userAgent: string,
|
||||
|
|
@ -44,7 +48,8 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
|||
readonly selectors: Selectors;
|
||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||
private _selectorsOwner: SelectorsOwner;
|
||||
_forwardPorts: number[] = [];
|
||||
private _sockets = new Map<string, net.Socket>();
|
||||
private _redirectPortForTest: number | undefined;
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
|
|
@ -61,17 +66,50 @@ 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));
|
||||
_enablePortForwarding(redirectPortForTest?: number) {
|
||||
this._redirectPortForTest = redirectPortForTest;
|
||||
this._channel.on('socksRequested', ({ uid, host, port }) => this._onSocksRequested(uid, host, port));
|
||||
this._channel.on('socksData', ({ uid, data }) => this._onSocksData(uid, Buffer.from(data, 'base64')));
|
||||
this._channel.on('socksClosed', ({ uid }) => this._onSocksClosed(uid));
|
||||
}
|
||||
|
||||
private async _onSocksRequested(uid: string, host: string, port: number): Promise<void> {
|
||||
try {
|
||||
if (this._redirectPortForTest)
|
||||
port = this._redirectPortForTest;
|
||||
const { address } = await dnsLookupAsync(host);
|
||||
const socket = await createSocket(address, port);
|
||||
socket.on('data', data => this._channel.socksData({ uid, data: data.toString('base64') }).catch(() => {}));
|
||||
socket.on('error', error => {
|
||||
this._channel.socksError({ uid, error: error.message }).catch(() => { });
|
||||
this._sockets.delete(uid);
|
||||
});
|
||||
socket.on('end', () => {
|
||||
this._channel.socksEnd({ uid }).catch(() => {});
|
||||
this._sockets.delete(uid);
|
||||
});
|
||||
const localAddress = socket.localAddress;
|
||||
const localPort = socket.localPort;
|
||||
this._sockets.set(uid, socket);
|
||||
this._channel.socksConnected({ uid, host: localAddress, port: localPort }).catch(() => {});
|
||||
} catch (error) {
|
||||
this._channel.socksFailed({ uid, errorCode: error.code }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
private _onSocksData(uid: string, data: Buffer): void {
|
||||
this._sockets.get(uid)?.write(data);
|
||||
}
|
||||
|
||||
static from(channel: channels.PlaywrightChannel): Playwright {
|
||||
return (channel as any)._object;
|
||||
}
|
||||
|
||||
async _enablePortForwarding(ports: number[]) {
|
||||
this._forwardPorts = ports;
|
||||
await this._channel.setForwardedPorts({ports});
|
||||
private _onSocksClosed(uid: string): void {
|
||||
this._sockets.get(uid)?.destroy();
|
||||
this._sockets.delete(uid);
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
/**
|
||||
* 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 { Playwright } from './playwright';
|
||||
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);
|
||||
assert(parent instanceof Playwright);
|
||||
|
||||
assert(parent._forwardPorts.includes(this._initializer.dstPort));
|
||||
assert(isLocalIpAddress(this._initializer.dstAddr));
|
||||
|
||||
if (isUnderTest() && process.env.PW_TEST_PROXY_TARGET)
|
||||
this._initializer.dstPort = Number(process.env.PW_TEST_PROXY_TARGET);
|
||||
|
||||
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());
|
||||
|
||||
this._connection.on('disconnect', () => 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -75,13 +75,11 @@ 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[],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import net from 'net';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { Playwright } from '../server/playwright';
|
||||
import { AndroidDispatcher } from './androidDispatcher';
|
||||
|
|
@ -22,10 +23,12 @@ import { Dispatcher, DispatcherScope } from './dispatcher';
|
|||
import { ElectronDispatcher } from './electronDispatcher';
|
||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||
import * as types from '../server/types';
|
||||
import { SocksSocketDispatcher } from './socksSocketDispatcher';
|
||||
import { SocksInterceptedSocketHandler } from '../server/socksServer';
|
||||
import { SocksConnection, SocksConnectionClient } from '../utils/socksProxy';
|
||||
import { createGuid } from '../utils/utils';
|
||||
|
||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightInitializer> implements channels.PlaywrightChannel {
|
||||
private _socksProxy: SocksProxy | undefined;
|
||||
|
||||
constructor(scope: DispatcherScope, playwright: Playwright, customSelectors?: channels.SelectorsChannel, preLaunchedBrowser?: channels.BrowserChannel) {
|
||||
const descriptors = require('../server/deviceDescriptors') as types.Devices;
|
||||
const deviceDescriptors = Object.entries(descriptors)
|
||||
|
|
@ -40,12 +43,81 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
|||
selectors: customSelectors || new SelectorsDispatcher(scope, playwright.selectors),
|
||||
preLaunchedBrowser,
|
||||
}, false);
|
||||
this._object.on('incomingSocksSocket', (socket: SocksInterceptedSocketHandler) => {
|
||||
this._dispatchEvent('incomingSocksSocket', { socket: new SocksSocketDispatcher(this, socket) });
|
||||
}
|
||||
|
||||
enableSocksProxy(port: number) {
|
||||
this._socksProxy = new SocksProxy(this);
|
||||
this._socksProxy.listen(port);
|
||||
}
|
||||
|
||||
async socksConnected(params: channels.PlaywrightSocksConnectedParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._socksProxy?.socketConnected(params);
|
||||
}
|
||||
|
||||
async socksFailed(params: channels.PlaywrightSocksFailedParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._socksProxy?.socketFailed(params);
|
||||
}
|
||||
|
||||
async socksData(params: channels.PlaywrightSocksDataParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._socksProxy?.sendSocketData(params);
|
||||
}
|
||||
|
||||
async socksError(params: channels.PlaywrightSocksErrorParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._socksProxy?.sendSocketError(params);
|
||||
}
|
||||
|
||||
async socksEnd(params: channels.PlaywrightSocksEndParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._socksProxy?.sendSocketEnd(params);
|
||||
}
|
||||
}
|
||||
|
||||
class SocksProxy implements SocksConnectionClient {
|
||||
private _server: net.Server;
|
||||
private _connections = new Map<string, SocksConnection>();
|
||||
private _dispatcher: PlaywrightDispatcher;
|
||||
|
||||
constructor(dispatcher: PlaywrightDispatcher) {
|
||||
this._dispatcher = dispatcher;
|
||||
this._server = new net.Server((socket: net.Socket) => {
|
||||
const uid = createGuid();
|
||||
const connection = new SocksConnection(uid, socket, this);
|
||||
this._connections.set(uid, connection);
|
||||
});
|
||||
}
|
||||
|
||||
async setForwardedPorts(params: channels.PlaywrightSetForwardedPortsParams): Promise<void> {
|
||||
this._object._setForwardedPorts(params.ports);
|
||||
listen(port: number) {
|
||||
this._server.listen(port);
|
||||
}
|
||||
|
||||
onSocketRequested(uid: string, host: string, port: number): void {
|
||||
this._dispatcher._dispatchEvent('socksRequested', { uid, host, port });
|
||||
}
|
||||
|
||||
onSocketData(uid: string, data: Buffer): void {
|
||||
this._dispatcher._dispatchEvent('socksData', { uid, data: data.toString('base64') });
|
||||
}
|
||||
|
||||
onSocketClosed(uid: string): void {
|
||||
this._dispatcher._dispatchEvent('socksClosed', { uid });
|
||||
}
|
||||
|
||||
socketConnected(params: channels.PlaywrightSocksConnectedParams) {
|
||||
this._connections.get(params.uid)?.socketConnected(params.host, params.port);
|
||||
}
|
||||
|
||||
socketFailed(params: channels.PlaywrightSocksFailedParams) {
|
||||
this._connections.get(params.uid)?.socketFailed(params.errorCode);
|
||||
}
|
||||
|
||||
sendSocketData(params: channels.PlaywrightSocksDataParams) {
|
||||
this._connections.get(params.uid)?.sendData(Buffer.from(params.data, 'base64'));
|
||||
}
|
||||
|
||||
sendSocketEnd(params: channels.PlaywrightSocksEndParams) {
|
||||
this._connections.get(params.uid)?.end();
|
||||
}
|
||||
|
||||
sendSocketError(params: channels.PlaywrightSocksErrorParams) {
|
||||
this._connections.get(params.uid)?.error(params.error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -196,19 +196,67 @@ export type PlaywrightInitializer = {
|
|||
preLaunchedBrowser?: BrowserChannel,
|
||||
};
|
||||
export interface PlaywrightChannel extends Channel {
|
||||
on(event: 'incomingSocksSocket', callback: (params: PlaywrightIncomingSocksSocketEvent) => void): this;
|
||||
setForwardedPorts(params: PlaywrightSetForwardedPortsParams, metadata?: Metadata): Promise<PlaywrightSetForwardedPortsResult>;
|
||||
on(event: 'socksRequested', callback: (params: PlaywrightSocksRequestedEvent) => void): this;
|
||||
on(event: 'socksData', callback: (params: PlaywrightSocksDataEvent) => void): this;
|
||||
on(event: 'socksClosed', callback: (params: PlaywrightSocksClosedEvent) => void): this;
|
||||
socksConnected(params: PlaywrightSocksConnectedParams, metadata?: Metadata): Promise<PlaywrightSocksConnectedResult>;
|
||||
socksFailed(params: PlaywrightSocksFailedParams, metadata?: Metadata): Promise<PlaywrightSocksFailedResult>;
|
||||
socksData(params: PlaywrightSocksDataParams, metadata?: Metadata): Promise<PlaywrightSocksDataResult>;
|
||||
socksError(params: PlaywrightSocksErrorParams, metadata?: Metadata): Promise<PlaywrightSocksErrorResult>;
|
||||
socksEnd(params: PlaywrightSocksEndParams, metadata?: Metadata): Promise<PlaywrightSocksEndResult>;
|
||||
}
|
||||
export type PlaywrightIncomingSocksSocketEvent = {
|
||||
socket: SocksSocketChannel,
|
||||
export type PlaywrightSocksRequestedEvent = {
|
||||
uid: string,
|
||||
host: string,
|
||||
port: number,
|
||||
};
|
||||
export type PlaywrightSetForwardedPortsParams = {
|
||||
ports: number[],
|
||||
export type PlaywrightSocksDataEvent = {
|
||||
uid: string,
|
||||
data: Binary,
|
||||
};
|
||||
export type PlaywrightSetForwardedPortsOptions = {
|
||||
export type PlaywrightSocksClosedEvent = {
|
||||
uid: string,
|
||||
};
|
||||
export type PlaywrightSocksConnectedParams = {
|
||||
uid: string,
|
||||
host: string,
|
||||
port: number,
|
||||
};
|
||||
export type PlaywrightSocksConnectedOptions = {
|
||||
|
||||
};
|
||||
export type PlaywrightSetForwardedPortsResult = void;
|
||||
export type PlaywrightSocksConnectedResult = void;
|
||||
export type PlaywrightSocksFailedParams = {
|
||||
uid: string,
|
||||
errorCode: string,
|
||||
};
|
||||
export type PlaywrightSocksFailedOptions = {
|
||||
|
||||
};
|
||||
export type PlaywrightSocksFailedResult = void;
|
||||
export type PlaywrightSocksDataParams = {
|
||||
uid: string,
|
||||
data: Binary,
|
||||
};
|
||||
export type PlaywrightSocksDataOptions = {
|
||||
|
||||
};
|
||||
export type PlaywrightSocksDataResult = void;
|
||||
export type PlaywrightSocksErrorParams = {
|
||||
uid: string,
|
||||
error: string,
|
||||
};
|
||||
export type PlaywrightSocksErrorOptions = {
|
||||
|
||||
};
|
||||
export type PlaywrightSocksErrorResult = void;
|
||||
export type PlaywrightSocksEndParams = {
|
||||
uid: string,
|
||||
};
|
||||
export type PlaywrightSocksEndOptions = {
|
||||
|
||||
};
|
||||
export type PlaywrightSocksEndResult = void;
|
||||
|
||||
// ----------- Selectors -----------
|
||||
export type SelectorsInitializer = {};
|
||||
|
|
@ -3317,44 +3365,6 @@ export type AndroidElementInfo = {
|
|||
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;
|
||||
|
||||
export const commandsWithTracingSnapshots = new Set([
|
||||
'EventTarget.waitForEventInfo',
|
||||
'BrowserContext.waitForEventInfo',
|
||||
|
|
|
|||
|
|
@ -380,18 +380,47 @@ Playwright:
|
|||
|
||||
commands:
|
||||
|
||||
setForwardedPorts:
|
||||
socksConnected:
|
||||
parameters:
|
||||
ports:
|
||||
type: array
|
||||
items: number
|
||||
uid: string
|
||||
host: string
|
||||
port: number
|
||||
|
||||
socksFailed:
|
||||
parameters:
|
||||
uid: string
|
||||
errorCode: string
|
||||
|
||||
socksData:
|
||||
parameters:
|
||||
uid: string
|
||||
data: binary
|
||||
|
||||
socksError:
|
||||
parameters:
|
||||
uid: string
|
||||
error: string
|
||||
|
||||
socksEnd:
|
||||
parameters:
|
||||
uid: string
|
||||
|
||||
events:
|
||||
|
||||
incomingSocksSocket:
|
||||
socksRequested:
|
||||
parameters:
|
||||
socket: SocksSocket
|
||||
uid: string
|
||||
host: string
|
||||
port: number
|
||||
|
||||
socksData:
|
||||
parameters:
|
||||
uid: string
|
||||
data: binary
|
||||
|
||||
socksClosed:
|
||||
parameters:
|
||||
uid: string
|
||||
|
||||
|
||||
Selectors:
|
||||
type: interface
|
||||
|
|
@ -2788,29 +2817,3 @@ 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:
|
||||
|
|
|
|||
|
|
@ -152,8 +152,25 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scheme.RootInitializeParams = tObject({
|
||||
language: tString,
|
||||
});
|
||||
scheme.PlaywrightSetForwardedPortsParams = tObject({
|
||||
ports: tArray(tNumber),
|
||||
scheme.PlaywrightSocksConnectedParams = tObject({
|
||||
uid: tString,
|
||||
host: tString,
|
||||
port: tNumber,
|
||||
});
|
||||
scheme.PlaywrightSocksFailedParams = tObject({
|
||||
uid: tString,
|
||||
errorCode: tString,
|
||||
});
|
||||
scheme.PlaywrightSocksDataParams = tObject({
|
||||
uid: tString,
|
||||
data: tBinary,
|
||||
});
|
||||
scheme.PlaywrightSocksErrorParams = tObject({
|
||||
uid: tString,
|
||||
error: tString,
|
||||
});
|
||||
scheme.PlaywrightSocksEndParams = tObject({
|
||||
uid: tString,
|
||||
});
|
||||
scheme.SelectorsRegisterParams = tObject({
|
||||
name: tString,
|
||||
|
|
@ -1315,14 +1332,6 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { Playwright } from '../client/playwright';
|
|||
|
||||
export type PlaywrightClientConnectOptions = {
|
||||
wsEndpoint: string;
|
||||
forwardPorts?: number[];
|
||||
timeout?: number
|
||||
};
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ export class PlaywrightClient {
|
|||
private _closePromise: Promise<void>;
|
||||
|
||||
static async connect(options: PlaywrightClientConnectOptions): Promise<PlaywrightClient> {
|
||||
const {wsEndpoint, forwardPorts, timeout = 30000} = options;
|
||||
const { wsEndpoint, timeout = 30000 } = options;
|
||||
const connection = new Connection();
|
||||
const ws = new WebSocket(wsEndpoint);
|
||||
connection.onmessage = message => ws.send(JSON.stringify(message));
|
||||
|
|
@ -40,8 +39,6 @@ export class PlaywrightClient {
|
|||
const playwrightClientPromise = new Promise<PlaywrightClient>((resolve, reject) => {
|
||||
ws.on('open', async () => {
|
||||
const playwright = await connection.initializePlaywright();
|
||||
if (forwardPorts)
|
||||
await playwright._enablePortForwarding(forwardPorts).catch(reject);
|
||||
resolve(new PlaywrightClient(playwright, ws));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,17 +31,12 @@ export interface PlaywrightServerDelegate {
|
|||
onClose: () => any;
|
||||
}
|
||||
|
||||
export type PlaywrightServerOptions = {
|
||||
acceptForwardedPorts?: boolean
|
||||
onDisconnect?: () => void;
|
||||
};
|
||||
|
||||
export class PlaywrightServer {
|
||||
private _wsServer: ws.Server | undefined;
|
||||
private _clientsCount = 0;
|
||||
private _delegate: PlaywrightServerDelegate;
|
||||
|
||||
static async startDefault({ acceptForwardedPorts, onDisconnect }: PlaywrightServerOptions = {}): Promise<PlaywrightServer> {
|
||||
static async startDefault(): Promise<PlaywrightServer> {
|
||||
const cleanup = async () => {
|
||||
await gracefullyCloseAll().catch(e => {});
|
||||
};
|
||||
|
|
@ -53,15 +48,14 @@ export class PlaywrightServer {
|
|||
let playwright: Playwright | undefined;
|
||||
new Root(connection, async (rootScope): Promise<PlaywrightDispatcher> => {
|
||||
playwright = createPlaywright();
|
||||
if (acceptForwardedPorts)
|
||||
await playwright._enablePortForwarding();
|
||||
return new PlaywrightDispatcher(rootScope, playwright);
|
||||
const dispatcher = new PlaywrightDispatcher(rootScope, playwright);
|
||||
if (process.env.PW_SOCKS_PROXY_PORT)
|
||||
dispatcher.enableSocksProxy(+process.env.PW_SOCKS_PROXY_PORT);
|
||||
return dispatcher;
|
||||
});
|
||||
return () => {
|
||||
cleanup();
|
||||
playwright?._disablePortForwarding();
|
||||
playwright?.selectors.unregisterAll();
|
||||
onDisconnect?.();
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export interface BrowserProcess {
|
|||
export type PlaywrightOptions = {
|
||||
rootSdkObject: SdkObject,
|
||||
selectors: Selectors,
|
||||
loopbackProxyOverride?: () => string,
|
||||
};
|
||||
|
||||
export type BrowserOptions = PlaywrightOptions & {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export abstract class BrowserType extends SdkObject {
|
|||
}
|
||||
|
||||
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
|
||||
options = validateLaunchOptions(options, this._playwrightOptions.loopbackProxyOverride?.());
|
||||
options = validateLaunchOptions(options);
|
||||
const controller = new ProgressController(metadata, this);
|
||||
controller.setLogName('browser');
|
||||
const browser = await controller.run(progress => {
|
||||
|
|
@ -63,7 +63,7 @@ export abstract class BrowserType extends SdkObject {
|
|||
}
|
||||
|
||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: types.LaunchPersistentOptions): Promise<BrowserContext> {
|
||||
options = validateLaunchOptions(options, this._playwrightOptions.loopbackProxyOverride?.());
|
||||
options = validateLaunchOptions(options);
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const persistent: types.BrowserContextOptions = options;
|
||||
controller.setLogName('browser');
|
||||
|
|
@ -257,14 +257,14 @@ function copyTestHooks(from: object, to: object) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateLaunchOptions<Options extends types.LaunchOptions>(options: Options, proxyOverride?: string): Options {
|
||||
function validateLaunchOptions<Options extends types.LaunchOptions>(options: Options): Options {
|
||||
const { devtools = false } = options;
|
||||
let { headless = !devtools, downloadsPath, proxy } = options;
|
||||
if (debugMode())
|
||||
headless = false;
|
||||
if (downloadsPath && !path.isAbsolute(downloadsPath))
|
||||
downloadsPath = path.join(process.cwd(), downloadsPath);
|
||||
if (proxyOverride)
|
||||
proxy = { server: proxyOverride };
|
||||
if (process.env.PW_SOCKS_PROXY_PORT)
|
||||
proxy = { server: `socks5://127.0.0.1:${process.env.PW_SOCKS_PROXY_PORT}` };
|
||||
return { ...options, devtools, headless, downloadsPath, proxy };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,14 +163,14 @@ export class Chromium extends BrowserType {
|
|||
const proxyURL = new URL(proxy.server);
|
||||
const isSocks = proxyURL.protocol === 'socks5:';
|
||||
// https://www.chromium.org/developers/design-documents/network-settings
|
||||
if (isSocks) {
|
||||
if (isSocks && !process.env.PW_SOCKS_PROXY_PORT) {
|
||||
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
|
||||
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
|
||||
}
|
||||
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||
const proxyBypassRules = [];
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
|
||||
if (this._playwrightOptions.loopbackProxyOverride)
|
||||
if (process.env.PW_SOCKS_PROXY_PORT)
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxy.bypass)
|
||||
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@ import { Selectors } from './selectors';
|
|||
import { WebKit } from './webkit/webkit';
|
||||
import { CallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { PortForwardingServer } from './socksSocket';
|
||||
import { SocksInterceptedSocketHandler } from './socksServer';
|
||||
import { assert } from '../utils/utils';
|
||||
|
||||
export class Playwright extends SdkObject {
|
||||
readonly selectors: Selectors;
|
||||
|
|
@ -36,7 +33,6 @@ export class Playwright extends SdkObject {
|
|||
readonly firefox: Firefox;
|
||||
readonly webkit: WebKit;
|
||||
readonly options: PlaywrightOptions;
|
||||
private _portForwardingServer: PortForwardingServer | undefined;
|
||||
|
||||
constructor(isInternal: boolean) {
|
||||
super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
|
||||
|
|
@ -56,27 +52,6 @@ export class Playwright extends SdkObject {
|
|||
this.android = new Android(new AdbBackend(), this.options);
|
||||
this.selectors = this.options.selectors;
|
||||
}
|
||||
|
||||
async _enablePortForwarding() {
|
||||
assert(!this._portForwardingServer);
|
||||
this._portForwardingServer = await PortForwardingServer.create(this);
|
||||
this.options.loopbackProxyOverride = () => this._portForwardingServer!.proxyServer();
|
||||
this._portForwardingServer.on('incomingSocksSocket', (socket: SocksInterceptedSocketHandler) => {
|
||||
this.emit('incomingSocksSocket', socket);
|
||||
});
|
||||
}
|
||||
|
||||
_disablePortForwarding() {
|
||||
if (!this._portForwardingServer)
|
||||
return;
|
||||
this._portForwardingServer.stop();
|
||||
}
|
||||
|
||||
_setForwardedPorts(ports: number[]) {
|
||||
if (!this._portForwardingServer)
|
||||
throw new Error(`Port forwarding needs to be enabled when launching the server via BrowserType.launchServer.`);
|
||||
this._portForwardingServer.setForwardedPorts(ports);
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlaywright(isInternal = false) {
|
||||
|
|
|
|||
|
|
@ -1,323 +0,0 @@
|
|||
/**
|
||||
* 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 { debugLogger } from '../utils/debugLogger';
|
||||
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 _parsingFinishedResolve!: (value?: unknown) => void;
|
||||
private _parsingFinishedReject!: (value: Error) => void;
|
||||
private _parsingFinished: 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._parsingFinished = new Promise((resolve, reject) => {
|
||||
this._parsingFinishedResolve = resolve;
|
||||
this._parsingFinishedReject = reject;
|
||||
});
|
||||
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++];
|
||||
const closeSocketOnError = () => {
|
||||
socket.end();
|
||||
this._parsingFinishedReject(new Error('Parsing aborted'));
|
||||
};
|
||||
while (i < chunk.length && this._phase !== ConnectionPhases.DONE) {
|
||||
switch (this._phase) {
|
||||
case ConnectionPhases.VERSION:
|
||||
if (readByte() !== SOCKS_VERSION)
|
||||
return closeSocketOnError();
|
||||
this._phase = ConnectionPhases.NMETHODS;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.NMETHODS:
|
||||
this._authMethods = Buffer.alloc(readByte());
|
||||
this._phase = ConnectionPhases.METHODS;
|
||||
break;
|
||||
|
||||
case ConnectionPhases.METHODS: {
|
||||
if (!this._authMethods)
|
||||
return closeSocketOnError();
|
||||
chunk.copy(this._authMethods, 0, i, i + chunk.length);
|
||||
if (!this._authMethods.includes(SOCKS_AUTH_METHOD.NO_AUTH))
|
||||
return closeSocketOnError();
|
||||
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;
|
||||
if (this._methodsp !== this._authMethods.length)
|
||||
return closeSocketOnError();
|
||||
if (i < chunk.length)
|
||||
this._socket.unshift(chunk.slice(i));
|
||||
this._authWithoutPassword(socket);
|
||||
this._phase = ConnectionPhases.REQ_CMD;
|
||||
break;
|
||||
}
|
||||
|
||||
case ConnectionPhases.REQ_CMD:
|
||||
if (readByte() !== SOCKS_VERSION)
|
||||
return closeSocketOnError();
|
||||
const cmd: SOCKS_CMD = readByte();
|
||||
if (cmd !== SOCKS_CMD.CONNECT)
|
||||
return closeSocketOnError();
|
||||
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();
|
||||
if (!(this._addressType in SOCKS_ATYP))
|
||||
return closeSocketOnError();
|
||||
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: {
|
||||
if (!this._dstAddr)
|
||||
return closeSocketOnError();
|
||||
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:
|
||||
if (!this._dstAddr)
|
||||
return closeSocketOnError();
|
||||
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._parsingFinishedResolve();
|
||||
return;
|
||||
default:
|
||||
return closeSocketOnError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _authWithoutPassword(socket: net.Socket) {
|
||||
socket.write(Buffer.from([0x05, 0x00]));
|
||||
}
|
||||
|
||||
async ready(): Promise<{ info: SocksConnectionInfo, forward: () => void, intercept: (parent: SdkObject) => SocksInterceptedSocketHandler }> {
|
||||
await this._parsingFinished;
|
||||
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 async listen(port: number, host?: string) {
|
||||
await new Promise(resolve => this.server.listen(port, host, resolve));
|
||||
}
|
||||
|
||||
async _handleConnection(incomingMessageHandler: IncomingProxyRequestHandler, socket: net.Socket) {
|
||||
const parser = new SocksV5ServerParser(socket);
|
||||
let parsedSocket;
|
||||
try {
|
||||
parsedSocket = await parser.ready();
|
||||
} catch (error) {
|
||||
debugLogger.log('proxy', `Could not parse: ${error} ${error?.stack}`);
|
||||
return;
|
||||
}
|
||||
incomingMessageHandler(parsedSocket.info, parsedSocket.forward, parsedSocket.intercept);
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
export class PortForwardingServer extends EventEmitter {
|
||||
private _forwardPorts: number[] = [];
|
||||
private _parent: SdkObject;
|
||||
private _server: SocksProxyServer;
|
||||
constructor(parent: SdkObject) {
|
||||
super();
|
||||
this.setMaxListeners(0);
|
||||
this._parent = parent;
|
||||
this._server = new SocksProxyServer(this._handler.bind(this));
|
||||
}
|
||||
|
||||
static async create(parent: SdkObject) {
|
||||
const server = new PortForwardingServer(parent);
|
||||
await server._server.listen(0);
|
||||
debugLogger.log('proxy', `starting server on port ${server._port()})`);
|
||||
return server;
|
||||
}
|
||||
|
||||
private _port(): number {
|
||||
return (this._server.server.address() as net.AddressInfo).port;
|
||||
}
|
||||
|
||||
public proxyServer() {
|
||||
return `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 setForwardedPorts(ports: number[]): void {
|
||||
debugLogger.log('proxy', `enable port forwarding on ports: ${ports}`);
|
||||
this._forwardPorts = ports;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
debugLogger.log('proxy', 'stopping server');
|
||||
this._server.close();
|
||||
}
|
||||
}
|
||||
25
src/utils/netUtils.ts
Normal file
25
src/utils/netUtils.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
export async function createSocket(host: string, port: number): Promise<net.Socket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
socket.on('connect', () => resolve(socket));
|
||||
socket.on('error', error => reject(error));
|
||||
});
|
||||
}
|
||||
270
src/utils/socksProxy.ts
Normal file
270
src/utils/socksProxy.ts
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
// https://tools.ietf.org/html/rfc1928
|
||||
|
||||
enum SocksAuth {
|
||||
NO_AUTHENTICATION_REQUIRED = 0x00,
|
||||
GSSAPI = 0x01,
|
||||
USERNAME_PASSWORD = 0x02,
|
||||
NO_ACCEPTABLE_METHODS = 0xFF
|
||||
}
|
||||
|
||||
enum SocksAddressType {
|
||||
IPv4 = 0x01,
|
||||
FqName = 0x03,
|
||||
IPv6 = 0x04
|
||||
}
|
||||
|
||||
enum SocksCommand {
|
||||
CONNECT = 0x01,
|
||||
BIND = 0x02,
|
||||
UDP_ASSOCIATE = 0x03
|
||||
}
|
||||
|
||||
enum SocksReply {
|
||||
Succeeded = 0x00,
|
||||
GeneralServerFailure = 0x01,
|
||||
NotAllowedByRuleSet = 0x02,
|
||||
NetworkUnreachable = 0x03,
|
||||
HostUnreachable = 0x04,
|
||||
ConnectionRefused = 0x05,
|
||||
TtlExpired = 0x06,
|
||||
CommandNotSupported = 0x07,
|
||||
AddressTypeNotSupported = 0x08
|
||||
}
|
||||
|
||||
export interface SocksConnectionClient {
|
||||
onSocketRequested(uid: string, host: string, port: number): void;
|
||||
onSocketData(uid: string, data: Buffer): void;
|
||||
onSocketClosed(uid: string): void;
|
||||
}
|
||||
|
||||
export class SocksConnection {
|
||||
private _buffer = Buffer.from([]);
|
||||
private _offset = 0;
|
||||
private _fence = 0;
|
||||
private _fenceCallback: (() => void) | undefined;
|
||||
private _socket: net.Socket;
|
||||
private _boundOnData: (buffer: Buffer) => void;
|
||||
private _uid: string;
|
||||
private _client: SocksConnectionClient;
|
||||
|
||||
constructor(uid: string, socket: net.Socket, client: SocksConnectionClient) {
|
||||
this._uid = uid;
|
||||
this._socket = socket;
|
||||
this._client = client;
|
||||
this._boundOnData = this._onData.bind(this);
|
||||
socket.on('data', this._boundOnData);
|
||||
socket.on('close', () => this._onClose());
|
||||
socket.on('end', () => this._onClose());
|
||||
socket.on('error', () => this._onClose());
|
||||
this._run().catch(() => this._socket.end());
|
||||
}
|
||||
|
||||
async _run() {
|
||||
assert(await this._authenticate());
|
||||
const { command, host, port } = await this._parseRequest();
|
||||
if (command !== SocksCommand.CONNECT) {
|
||||
this._writeBytes(Buffer.from([
|
||||
0x05,
|
||||
SocksReply.CommandNotSupported,
|
||||
0x00, // RSV
|
||||
0x01, // IPv4
|
||||
0x00, 0x00, 0x00, 0x00, // Address
|
||||
0x00, 0x00 // Port
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
this._socket.off('data', this._boundOnData);
|
||||
this._client.onSocketRequested(this._uid, host, port);
|
||||
}
|
||||
|
||||
async _authenticate(): Promise<boolean> {
|
||||
// Request:
|
||||
// +----+----------+----------+
|
||||
// |VER | NMETHODS | METHODS |
|
||||
// +----+----------+----------+
|
||||
// | 1 | 1 | 1 to 255 |
|
||||
// +----+----------+----------+
|
||||
|
||||
// Response:
|
||||
// +----+--------+
|
||||
// |VER | METHOD |
|
||||
// +----+--------+
|
||||
// | 1 | 1 |
|
||||
// +----+--------+
|
||||
|
||||
const version = await this._readByte();
|
||||
assert(version === 0x05, 'The VER field must be set to x05 for this version of the protocol, was ' + version);
|
||||
|
||||
const nMethods = await this._readByte();
|
||||
assert(nMethods, 'No authentication methods specified');
|
||||
|
||||
const methods = await this._readBytes(nMethods);
|
||||
for (const method of methods) {
|
||||
if (method === 0) {
|
||||
this._writeBytes(Buffer.from([version, method]));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this._writeBytes(Buffer.from([version, SocksAuth.NO_ACCEPTABLE_METHODS]));
|
||||
return false;
|
||||
}
|
||||
|
||||
async _parseRequest(): Promise<{ host: string, port: number, command: SocksCommand }> {
|
||||
// Request.
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
|
||||
// Response.
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
|
||||
const version = await this._readByte();
|
||||
assert(version === 0x05, 'The VER field must be set to x05 for this version of the protocol, was ' + version);
|
||||
|
||||
const command = await this._readByte();
|
||||
await this._readByte(); // skip reserved.
|
||||
const addressType = await this._readByte();
|
||||
let host = '';
|
||||
switch (addressType) {
|
||||
case SocksAddressType.IPv4:
|
||||
host = (await this._readBytes(4)).join('.');
|
||||
break;
|
||||
case SocksAddressType.FqName:
|
||||
const length = await this._readByte();
|
||||
host = (await this._readBytes(length)).toString();
|
||||
break;
|
||||
case SocksAddressType.IPv6:
|
||||
const bytes = await this._readBytes(16);
|
||||
const tokens = [];
|
||||
for (let i = 0; i < 8; ++i)
|
||||
tokens.push(bytes.readUInt16BE(i * 2));
|
||||
host = tokens.join(':');
|
||||
break;
|
||||
}
|
||||
const port = (await this._readBytes(2)).readUInt16BE(0);
|
||||
|
||||
this._buffer = Buffer.from([]);
|
||||
this._offset = 0;
|
||||
this._fence = 0;
|
||||
|
||||
return {
|
||||
command,
|
||||
host,
|
||||
port
|
||||
};
|
||||
}
|
||||
|
||||
private async _readByte(): Promise<number> {
|
||||
const buffer = await this._readBytes(1);
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
private async _readBytes(length: number): Promise<Buffer> {
|
||||
this._fence = this._offset + length;
|
||||
if (!this._buffer || this._buffer.length < this._fence)
|
||||
await new Promise<void>(f => this._fenceCallback = f);
|
||||
this._offset += length;
|
||||
return this._buffer.slice(this._offset - length, this._offset);
|
||||
}
|
||||
|
||||
private _writeBytes(buffer: Buffer) {
|
||||
if (this._socket.writable)
|
||||
this._socket.write(buffer);
|
||||
}
|
||||
|
||||
private _onClose() {
|
||||
this._client.onSocketClosed(this._uid);
|
||||
}
|
||||
|
||||
private _onData(buffer: Buffer) {
|
||||
this._buffer = Buffer.concat([this._buffer, buffer]);
|
||||
if (this._fenceCallback && this._buffer.length >= this._fence) {
|
||||
const callback = this._fenceCallback;
|
||||
this._fenceCallback = undefined;
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
socketConnected(host: string, port: number) {
|
||||
this._writeBytes(Buffer.from([
|
||||
0x05,
|
||||
SocksReply.Succeeded,
|
||||
0x00, // RSV
|
||||
0x01, // IPv4
|
||||
...parseIP(host), // Address
|
||||
port << 8, port & 0xFF // Port
|
||||
]));
|
||||
this._socket.on('data', data => this._client.onSocketData(this._uid, data));
|
||||
}
|
||||
|
||||
socketFailed(errorCode: string) {
|
||||
const buffer = Buffer.from([
|
||||
0x05,
|
||||
0,
|
||||
0x00, // RSV
|
||||
0x01, // IPv4
|
||||
...parseIP('0.0.0.0'), // Address
|
||||
0, 0 // Port
|
||||
]);
|
||||
switch (errorCode) {
|
||||
case 'ENOENT':
|
||||
case 'ENOTFOUND':
|
||||
case 'ETIMEDOUT':
|
||||
case 'EHOSTUNREACH':
|
||||
buffer[1] = SocksReply.HostUnreachable;
|
||||
break;
|
||||
case 'ENETUNREACH':
|
||||
buffer[1] = SocksReply.NetworkUnreachable;
|
||||
break;
|
||||
case 'ECONNREFUSED':
|
||||
buffer[1] = SocksReply.ConnectionRefused;
|
||||
break;
|
||||
}
|
||||
this._writeBytes(buffer);
|
||||
this._socket.end();
|
||||
}
|
||||
|
||||
sendData(data: Buffer) {
|
||||
this._socket.write(data);
|
||||
}
|
||||
|
||||
end() {
|
||||
this._socket.end();
|
||||
}
|
||||
|
||||
error(error: string) {
|
||||
this._socket.destroy(new Error(error));
|
||||
}
|
||||
}
|
||||
|
||||
function parseIP(address: string): number[] {
|
||||
if (!net.isIPv4(address))
|
||||
throw new Error('IPv6 is not supported');
|
||||
return address.split('.', 4).map(t => +t);
|
||||
}
|
||||
130
tests/port-forwarding-server.spec.ts
Normal file
130
tests/port-forwarding-server.spec.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* 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 childProcess from 'child_process';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
import net from 'net';
|
||||
|
||||
import { contextTest, expect } from './config/browserTest';
|
||||
import { PlaywrightClient } from '../lib/remote/playwrightClient';
|
||||
import type { Page } from '..';
|
||||
|
||||
class OutOfProcessPlaywrightServer {
|
||||
private _driverProcess: childProcess.ChildProcess;
|
||||
private _receivedPortPromise: Promise<string>;
|
||||
|
||||
constructor(port: number, proxyPort: number) {
|
||||
this._driverProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'cli', 'cli.js'), ['run-server', port.toString()], {
|
||||
stdio: 'pipe',
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
PW_SOCKS_PROXY_PORT: String(proxyPort)
|
||||
}
|
||||
});
|
||||
this._driverProcess.unref();
|
||||
this._receivedPortPromise = new Promise<string>((resolve, reject) => {
|
||||
this._driverProcess.stdout.on('data', (data: Buffer) => {
|
||||
const prefix = 'Listening on ';
|
||||
const line = data.toString();
|
||||
if (line.startsWith(prefix))
|
||||
resolve(line.substr(prefix.length));
|
||||
});
|
||||
this._driverProcess.on('exit', () => reject());
|
||||
});
|
||||
}
|
||||
async kill() {
|
||||
const waitForExit = new Promise<void>(resolve => this._driverProcess.on('exit', () => resolve()));
|
||||
this._driverProcess.kill('SIGKILL');
|
||||
await waitForExit;
|
||||
}
|
||||
public async wsEndpoint(): Promise<string> {
|
||||
return await this._receivedPortPromise;
|
||||
}
|
||||
}
|
||||
|
||||
const it = contextTest.extend<{ pageFactory: (redirectPortForTest?: number) => Promise<Page> }>({
|
||||
pageFactory: async ({ browserName, browserOptions }, run, testInfo) => {
|
||||
const playwrightServers: OutOfProcessPlaywrightServer[] = [];
|
||||
await run(async (redirectPortForTest?: number): Promise<Page> => {
|
||||
const server = new OutOfProcessPlaywrightServer(0, 3200 + testInfo.workerIndex);
|
||||
playwrightServers.push(server);
|
||||
const service = await PlaywrightClient.connect({
|
||||
wsEndpoint: await server.wsEndpoint(),
|
||||
});
|
||||
const playwright = service.playwright();
|
||||
playwright._enablePortForwarding(redirectPortForTest);
|
||||
const browser = await playwright[browserName].launch(browserOptions);
|
||||
return await browser.newPage();
|
||||
});
|
||||
for (const playwrightServer of playwrightServers)
|
||||
await playwrightServer.kill();
|
||||
},
|
||||
});
|
||||
|
||||
it.fixme(({ platform, browserName }) => browserName === 'webkit' && platform !== 'linux');
|
||||
it.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
async function startTestServer() {
|
||||
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
res.end('<html><body>from-retargeted-server</body></html>');
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, resolve));
|
||||
return {
|
||||
testServerPort: (server.address() as net.AddressInfo).port,
|
||||
stopTestServer: () => server.close()
|
||||
};
|
||||
}
|
||||
|
||||
it('should forward non-forwarded requests', async ({ pageFactory, server }) => {
|
||||
let reachedOriginalTarget = false;
|
||||
server.setRoute('/foo.html', async (req, res) => {
|
||||
reachedOriginalTarget = true;
|
||||
res.end('<html><body>original-target</body></html>');
|
||||
});
|
||||
const page = await pageFactory();
|
||||
await page.goto(server.PREFIX + '/foo.html');
|
||||
expect(await page.content()).toContain('original-target');
|
||||
expect(reachedOriginalTarget).toBe(true);
|
||||
});
|
||||
|
||||
it('should proxy local requests', async ({ pageFactory, server }, workerInfo) => {
|
||||
const { testServerPort, stopTestServer } = await startTestServer();
|
||||
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 page = await pageFactory(testServerPort);
|
||||
await page.goto(`http://localhost:${examplePort}/foo.html`);
|
||||
expect(await page.content()).toContain('from-retargeted-server');
|
||||
expect(reachedOriginalTarget).toBe(false);
|
||||
stopTestServer();
|
||||
});
|
||||
|
||||
it('should lead to the error page for forwarded requests when the connection is refused', async ({ pageFactory, browserName }, workerInfo) => {
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const page = await pageFactory();
|
||||
const error = await page.goto(`http://localhost:${examplePort}`).catch(e => e);
|
||||
if (browserName === 'chromium')
|
||||
expect(error.message).toContain('net::ERR_SOCKS_CONNECTION_FAILED at http://localhost:20');
|
||||
else if (browserName === 'webkit')
|
||||
expect(error.message).toBeTruthy();
|
||||
else if (browserName === 'firefox')
|
||||
expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED');
|
||||
});
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
/**
|
||||
* 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 childProcess from 'child_process';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import net from 'net';
|
||||
|
||||
import { contextTest, expect } from './config/browserTest';
|
||||
import { PlaywrightClient } from '../lib/remote/playwrightClient';
|
||||
import { createGuid } from '../src/utils/utils';
|
||||
import type { PlaywrightServerOptions } from '../src/remote/playwrightServer';
|
||||
import type { LaunchOptions, ConnectOptions } from '../index';
|
||||
import type { Page, BrowserServer } from '..';
|
||||
|
||||
class OutOfProcessPlaywrightServer {
|
||||
private _driverProcess: childProcess.ChildProcess;
|
||||
private _receivedPortPromise: Promise<string>;
|
||||
constructor(port: number, config: PlaywrightServerOptions) {
|
||||
const configFile = path.join(os.tmpdir(), `playwright-server-config-${createGuid()}.json`);
|
||||
fs.writeFileSync(configFile, JSON.stringify(config));
|
||||
this._driverProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'cli', 'cli.js'), ['run-server', port.toString(), configFile], {
|
||||
stdio: 'pipe',
|
||||
detached: true,
|
||||
});
|
||||
this._driverProcess.unref();
|
||||
this._receivedPortPromise = new Promise<string>((resolve, reject) => {
|
||||
this._driverProcess.stdout.on('data', (data: Buffer) => {
|
||||
const prefix = 'Listening on ';
|
||||
const line = data.toString();
|
||||
if (line.startsWith(prefix))
|
||||
resolve(line.substr(prefix.length));
|
||||
});
|
||||
this._driverProcess.on('exit', () => reject());
|
||||
});
|
||||
}
|
||||
async kill() {
|
||||
const waitForExit = new Promise<void>(resolve => this._driverProcess.on('exit', () => resolve()));
|
||||
this._driverProcess.kill('SIGKILL');
|
||||
await waitForExit;
|
||||
}
|
||||
public async wsEndpoint(): Promise<string> {
|
||||
return await this._receivedPortPromise;
|
||||
}
|
||||
}
|
||||
|
||||
type PageFactoryOptions = {
|
||||
acceptForwardedPorts: boolean
|
||||
forwardPorts: number[]
|
||||
};
|
||||
|
||||
type LaunchMode = 'playwrightclient' | 'launchServer';
|
||||
|
||||
const it = contextTest.extend<{ pageFactory: (options?: PageFactoryOptions) => Promise<Page>, launchMode: LaunchMode }>({
|
||||
launchMode: [ 'launchServer', { scope: 'test' }],
|
||||
pageFactory: async ({ launchMode, browserType, browserName, browserOptions }, run) => {
|
||||
const browserServers: BrowserServer[] = [];
|
||||
const playwrightServers: OutOfProcessPlaywrightServer[] = [];
|
||||
await run(async (options?: PageFactoryOptions): Promise<Page> => {
|
||||
const { acceptForwardedPorts, forwardPorts } = options;
|
||||
if (launchMode === 'playwrightclient') {
|
||||
const server = new OutOfProcessPlaywrightServer(0, {
|
||||
acceptForwardedPorts,
|
||||
});
|
||||
playwrightServers.push(server);
|
||||
const service = await PlaywrightClient.connect({
|
||||
wsEndpoint: await server.wsEndpoint(),
|
||||
forwardPorts,
|
||||
});
|
||||
const playwright = service.playwright();
|
||||
const browser = await playwright[browserName].launch(browserOptions);
|
||||
return await browser.newPage();
|
||||
}
|
||||
const browserServer = await browserType.launchServer({
|
||||
...browserOptions,
|
||||
_acceptForwardedPorts: acceptForwardedPorts
|
||||
} as LaunchOptions);
|
||||
browserServers.push(browserServer);
|
||||
const browser = await browserType.connect({
|
||||
wsEndpoint: browserServer.wsEndpoint(),
|
||||
_forwardPorts: forwardPorts
|
||||
} as ConnectOptions);
|
||||
return await browser.newPage();
|
||||
});
|
||||
for (const browserServer of browserServers)
|
||||
await browserServer.close();
|
||||
for (const playwrightServer of playwrightServers)
|
||||
await playwrightServer.kill();
|
||||
},
|
||||
});
|
||||
|
||||
it.fixme(({ platform, browserName }) => platform === 'darwin' && browserName === 'webkit');
|
||||
it.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
it.beforeEach(() => {
|
||||
delete process.env.PW_TEST_PROXY_TARGET;
|
||||
});
|
||||
|
||||
async function startTestServer() {
|
||||
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
res.end('<html><body>from-retargeted-server</body></html>');
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, resolve));
|
||||
return {
|
||||
testServerPort: (server.address() as net.AddressInfo).port,
|
||||
stopTestServer: () => server.close()
|
||||
};
|
||||
}
|
||||
|
||||
for (const launchMode of ['playwrightclient', 'launchServer'] as LaunchMode[]) {
|
||||
it.describe(`${launchMode}:`, () => {
|
||||
it.use({ launchMode });
|
||||
|
||||
it('should forward non-forwarded requests', async ({ pageFactory, server }) => {
|
||||
let reachedOriginalTarget = false;
|
||||
server.setRoute('/foo.html', async (req, res) => {
|
||||
reachedOriginalTarget = true;
|
||||
res.end('<html><body>original-target</body></html>');
|
||||
});
|
||||
const page = await pageFactory({ acceptForwardedPorts: true, forwardPorts: [] });
|
||||
await page.goto(server.PREFIX + '/foo.html');
|
||||
expect(await page.content()).toContain('original-target');
|
||||
expect(reachedOriginalTarget).toBe(true);
|
||||
});
|
||||
|
||||
it('should proxy local requests', async ({ pageFactory, server }, workerInfo) => {
|
||||
const { testServerPort, stopTestServer } = await startTestServer();
|
||||
process.env.PW_TEST_PROXY_TARGET = testServerPort.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 page = await pageFactory({ acceptForwardedPorts: true, forwardPorts: [examplePort] });
|
||||
await page.goto(`http://localhost:${examplePort}/foo.html`);
|
||||
expect(await page.content()).toContain('from-retargeted-server');
|
||||
expect(reachedOriginalTarget).toBe(false);
|
||||
stopTestServer();
|
||||
});
|
||||
|
||||
it('should lead to the error page for forwarded requests when the connection is refused', async ({ pageFactory }, workerInfo) => {
|
||||
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||
const page = await pageFactory({ acceptForwardedPorts: true, forwardPorts: [examplePort] });
|
||||
const response = await page.goto(`http://localhost:${examplePort}`);
|
||||
expect(response.status()).toBe(502);
|
||||
await page.waitForSelector('text=Connection error');
|
||||
});
|
||||
|
||||
it('should lead to the error page for non-forwarded requests when the connection is refused', async ({ pageFactory }) => {
|
||||
process.env.PW_TEST_PROXY_TARGET = '50001';
|
||||
const page = await pageFactory({ acceptForwardedPorts: true, forwardPorts: [] });
|
||||
const response = await page.goto(`http://localhost:44123/non-existing-url`);
|
||||
expect(response.status()).toBe(502);
|
||||
await page.waitForSelector('text=Connection error');
|
||||
});
|
||||
|
||||
it('should should not allow to connect when the server does not allow port-forwarding', async ({ pageFactory }) => {
|
||||
await expect(pageFactory({ acceptForwardedPorts: false, forwardPorts: [] })).rejects.toThrowError('Port forwarding needs to be enabled when launching the server via BrowserType.launchServer.');
|
||||
await expect(pageFactory({ acceptForwardedPorts: false, forwardPorts: [1234] })).rejects.toThrowError('Port forwarding needs to be enabled when launching the server via BrowserType.launchServer.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('launchServer: 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();
|
||||
});
|
||||
Loading…
Reference in a new issue