chore: move connect utility into localUtils (#17590)

This commit is contained in:
Pavel Feldman 2022-09-27 12:05:06 -08:00 committed by GitHub
parent 3409a37f77
commit 06e73b414f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 193 deletions

View File

@ -143,10 +143,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
let browser: Browser;
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
const connectParams: channels.BrowserTypeConnectParams = { wsEndpoint, headers, slowMo: params.slowMo, timeout: params.timeout };
const localUtils = this._connection.localUtils();
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: params.slowMo, timeout: params.timeout };
if ((params as any).__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const { pipe } = await this._channel.connect(connectParams);
const { pipe } = await localUtils._channel.connect(connectParams);
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(this._connection.localUtils());
connection.markAsRemote();

View File

@ -245,6 +245,16 @@ scheme.LocalUtilsHarUnzipParams = tObject({
harFile: tString,
});
scheme.LocalUtilsHarUnzipResult = tOptional(tObject({}));
scheme.LocalUtilsConnectParams = tObject({
wsEndpoint: tString,
headers: tOptional(tAny),
slowMo: tOptional(tNumber),
timeout: tOptional(tNumber),
socksProxyRedirectPortForTest: tOptional(tNumber),
});
scheme.LocalUtilsConnectResult = tObject({
pipe: tChannel(['JsonPipe']),
});
scheme.RootInitializer = tOptional(tObject({}));
scheme.RootInitializeParams = tObject({
sdkLanguage: tString,
@ -414,16 +424,6 @@ scheme.BrowserTypeInitializer = tObject({
executablePath: tString,
name: tString,
});
scheme.BrowserTypeConnectParams = tObject({
wsEndpoint: tString,
headers: tOptional(tAny),
slowMo: tOptional(tNumber),
timeout: tOptional(tNumber),
socksProxyRedirectPortForTest: tOptional(tNumber),
});
scheme.BrowserTypeConnectResult = tObject({
pipe: tChannel(['JsonPipe']),
});
scheme.BrowserTypeLaunchParams = tObject({
channel: tOptional(tString),
executablePath: tOptional(tString),

View File

@ -21,17 +21,6 @@ import type { RootDispatcher } from './dispatcher';
import { Dispatcher } from './dispatcher';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import type { CallMetadata } from '../instrumentation';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import { getUserAgent } from '../../common/userAgent';
import * as socks from '../../common/socksProxy';
import EventEmitter from 'events';
import { ProgressController } from '../progress';
import type { Progress } from '../progress';
import { WebSocketTransport } from '../transport';
import { findValidator, ValidationError, type ValidatorContext } from '../../protocol/validator';
import { fetchData } from '../../common/netUtils';
import type { HTTPRequestParams } from '../../common/netUtils';
import type http from 'http';
export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeChannel, RootDispatcher> implements channels.BrowserTypeChannel {
_type_BrowserType = true;
@ -60,134 +49,4 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
defaultContext: browser._defaultContext ? new BrowserContextDispatcher(browserDispatcher, browser._defaultContext) : undefined,
};
}
async connect(params: channels.BrowserTypeConnectParams, metadata: CallMetadata): Promise<channels.BrowserTypeConnectResult> {
const controller = new ProgressController(metadata, this._object);
controller.setLogName('browser');
return await controller.run(async progress => {
const paramsHeaders = Object.assign({ 'User-Agent': getUserAgent() }, params.headers || {});
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
const transport = await WebSocketTransport.connect(progress, wsEndpoint, paramsHeaders, true);
let socksInterceptor: SocksInterceptor | undefined;
const pipe = new JsonPipeDispatcher(this);
transport.onmessage = json => {
if (json.method === '__create__' && json.params.type === 'SocksSupport')
socksInterceptor = new SocksInterceptor(transport, params.socksProxyRedirectPortForTest, json.params.guid);
if (socksInterceptor?.interceptMessage(json))
return;
const cb = () => {
try {
pipe.dispatch(json);
} catch (e) {
transport.close();
}
};
if (params.slowMo)
setTimeout(cb, params.slowMo);
else
cb();
};
pipe.on('message', message => {
transport.send(message);
});
transport.onclose = () => {
socksInterceptor?.cleanup();
pipe.wasClosed();
};
pipe.on('close', () => transport.close());
return { pipe };
}, params.timeout || 0);
}
}
class SocksInterceptor {
private _handler: socks.SocksProxyHandler;
private _channel: channels.SocksSupportChannel & EventEmitter;
private _socksSupportObjectGuid: string;
private _ids = new Set<number>();
constructor(transport: WebSocketTransport, redirectPortForTest: number | undefined, socksSupportObjectGuid: string) {
this._handler = new socks.SocksProxyHandler(redirectPortForTest);
this._socksSupportObjectGuid = socksSupportObjectGuid;
let lastId = -1;
this._channel = new Proxy(new EventEmitter(), {
get: (obj: any, prop) => {
if ((prop in obj) || obj[prop] !== undefined || typeof prop !== 'string')
return obj[prop];
return (params: any) => {
try {
const id = --lastId;
this._ids.add(id);
const validator = findValidator('SocksSupport', prop, 'Params');
params = validator(params, '', { tChannelImpl: tChannelForSocks, binary: 'toBase64' });
transport.send({ id, guid: socksSupportObjectGuid, method: prop, params, metadata: { stack: [], apiName: '', internal: true } } as any);
} catch (e) {
}
};
},
}) as channels.SocksSupportChannel & EventEmitter;
this._handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => this._channel.socksConnected(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => this._channel.socksData(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => this._channel.socksError(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => this._channel.socksFailed(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => this._channel.socksEnd(payload));
this._channel.on('socksRequested', payload => this._handler.socketRequested(payload));
this._channel.on('socksClosed', payload => this._handler.socketClosed(payload));
this._channel.on('socksData', payload => this._handler.sendSocketData(payload));
}
cleanup() {
this._handler.cleanup();
}
interceptMessage(message: any): boolean {
if (this._ids.has(message.id)) {
this._ids.delete(message.id);
return true;
}
if (message.guid === this._socksSupportObjectGuid) {
const validator = findValidator('SocksSupport', message.method, 'Event');
const params = validator(message.params, '', { tChannelImpl: tChannelForSocks, binary: 'fromBase64' });
this._channel.emit(message.method, params);
return true;
}
return false;
}
}
function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
throw new ValidationError(`${path}: channels are not expected in SocksSupport`);
}
async function urlToWSEndpoint(progress: Progress, endpointURL: string): Promise<string> {
if (endpointURL.startsWith('ws'))
return endpointURL;
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
const fetchUrl = new URL(endpointURL);
if (!fetchUrl.pathname.endsWith('/'))
fetchUrl.pathname += '/';
fetchUrl.pathname += 'json';
const json = await fetchData({
url: fetchUrl.toString(),
method: 'GET',
timeout: progress.timeUntilDeadline(),
headers: { 'User-Agent': getUserAgent() },
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
`This does not look like a Playwright server, try connecting via ws://.`);
});
progress.throwIfAborted();
const wsUrl = new URL(endpointURL);
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
if (wsEndpointPath.startsWith('/'))
wsEndpointPath = wsEndpointPath.substring(1);
if (!wsUrl.pathname.endsWith('/'))
wsUrl.pathname += '/';
wsUrl.pathname += wsEndpointPath;
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
return wsUrl.toString();
}

View File

@ -18,11 +18,11 @@ import type * as channels from '@protocol/channels';
import { Dispatcher } from './dispatcher';
import { createGuid } from '../../utils';
import { serializeError } from '../../protocol/serializers';
import type { BrowserTypeDispatcher } from './browserTypeDispatcher';
import type { LocalUtilsDispatcher } from './localUtilsDispatcher';
export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.JsonPipeChannel, BrowserTypeDispatcher> implements channels.JsonPipeChannel {
export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.JsonPipeChannel, LocalUtilsDispatcher> implements channels.JsonPipeChannel {
_type_JsonPipe = true;
constructor(scope: BrowserTypeDispatcher) {
constructor(scope: LocalUtilsDispatcher) {
super(scope, { guid: 'jsonPipe@' + createGuid() }, 'JsonPipe', {});
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import type EventEmitter from 'events';
import EventEmitter from 'events';
import fs from 'fs';
import path from 'path';
import type * as channels from '@protocol/channels';
@ -26,17 +26,32 @@ import { yazl, yauzl } from '../../zipBundle';
import { ZipFile } from '../../utils/zipFile';
import type * as har from '@trace/har';
import type { HeadersArray } from '../types';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import * as socks from '../../common/socksProxy';
import { WebSocketTransport } from '../transport';
import type { CallMetadata } from '../instrumentation';
import { getUserAgent } from '../../common/userAgent';
import type { Progress } from '../progress';
import { ProgressController } from '../progress';
import { findValidator, ValidationError } from '../../protocol/validator';
import type { ValidatorContext } from '../../protocol/validator';
import { fetchData } from '../../common/netUtils';
import type { HTTPRequestParams } from '../../common/netUtils';
import type http from 'http';
import type { Playwright } from '../playwright';
import { SdkObject } from '../../server/instrumentation';
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
_type_LocalUtils: boolean;
private _harBakends = new Map<string, HarBackend>();
constructor(scope: RootDispatcher) {
super(scope, { guid: 'localUtils@' + createGuid() }, 'LocalUtils', {});
constructor(scope: RootDispatcher, playwright: Playwright) {
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
super(scope, localUtils, 'LocalUtils', {});
this._type_LocalUtils = true;
}
async zip(params: channels.LocalUtilsZipParams, metadata?: channels.Metadata): Promise<void> {
async zip(params: channels.LocalUtilsZipParams, metadata: CallMetadata): Promise<void> {
const promise = new ManualPromise<void>();
const zipFile = new yazl.ZipFile();
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
@ -91,7 +106,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
return promise;
}
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata?: channels.Metadata): Promise<channels.LocalUtilsHarOpenResult> {
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
let harBackend: HarBackend;
if (params.file.endsWith('.zip')) {
const zipFile = new ZipFile(params.file);
@ -110,14 +125,14 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
return { harId: harBackend.id };
}
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata?: channels.Metadata): Promise<channels.LocalUtilsHarLookupResult> {
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
const harBackend = this._harBakends.get(params.harId);
if (!harBackend)
return { action: 'error', message: `Internal error: har was not opened` };
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
}
async harClose(params: channels.LocalUtilsHarCloseParams, metadata?: channels.Metadata): Promise<void> {
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
const harBackend = this._harBakends.get(params.harId);
if (harBackend) {
this._harBakends.delete(harBackend.id);
@ -125,7 +140,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
}
}
async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata?: channels.Metadata): Promise<void> {
async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata: CallMetadata): Promise<void> {
const dir = path.dirname(params.zipFile);
const zipFile = new ZipFile(params.zipFile);
for (const entry of await zipFile.entries()) {
@ -138,6 +153,46 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
zipFile.close();
await fs.promises.unlink(params.zipFile);
}
async connect(params: channels.LocalUtilsConnectParams, metadata: CallMetadata): Promise<channels.LocalUtilsConnectResult> {
const controller = new ProgressController(metadata, this._object as SdkObject);
controller.setLogName('browser');
return await controller.run(async progress => {
const paramsHeaders = Object.assign({ 'User-Agent': getUserAgent() }, params.headers || {});
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
const transport = await WebSocketTransport.connect(progress, wsEndpoint, paramsHeaders, true);
let socksInterceptor: SocksInterceptor | undefined;
const pipe = new JsonPipeDispatcher(this);
transport.onmessage = json => {
if (json.method === '__create__' && json.params.type === 'SocksSupport')
socksInterceptor = new SocksInterceptor(transport, params.socksProxyRedirectPortForTest, json.params.guid);
if (socksInterceptor?.interceptMessage(json))
return;
const cb = () => {
try {
pipe.dispatch(json);
} catch (e) {
transport.close();
}
};
if (params.slowMo)
setTimeout(cb, params.slowMo);
else
cb();
};
pipe.on('message', message => {
transport.send(message);
});
transport.onclose = () => {
socksInterceptor?.cleanup();
pipe.wasClosed();
};
pipe.on('close', () => transport.close());
return { pipe };
}, params.timeout || 0);
}
}
const redirectStatus = [301, 302, 303, 307, 308];
@ -262,6 +317,62 @@ class HarBackend {
}
}
class SocksInterceptor {
private _handler: socks.SocksProxyHandler;
private _channel: channels.SocksSupportChannel & EventEmitter;
private _socksSupportObjectGuid: string;
private _ids = new Set<number>();
constructor(transport: WebSocketTransport, redirectPortForTest: number | undefined, socksSupportObjectGuid: string) {
this._handler = new socks.SocksProxyHandler(redirectPortForTest);
this._socksSupportObjectGuid = socksSupportObjectGuid;
let lastId = -1;
this._channel = new Proxy(new EventEmitter(), {
get: (obj: any, prop) => {
if ((prop in obj) || obj[prop] !== undefined || typeof prop !== 'string')
return obj[prop];
return (params: any) => {
try {
const id = --lastId;
this._ids.add(id);
const validator = findValidator('SocksSupport', prop, 'Params');
params = validator(params, '', { tChannelImpl: tChannelForSocks, binary: 'toBase64' });
transport.send({ id, guid: socksSupportObjectGuid, method: prop, params, metadata: { stack: [], apiName: '', internal: true } } as any);
} catch (e) {
}
};
},
}) as channels.SocksSupportChannel & EventEmitter;
this._handler.on(socks.SocksProxyHandler.Events.SocksConnected, (payload: socks.SocksSocketConnectedPayload) => this._channel.socksConnected(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksData, (payload: socks.SocksSocketDataPayload) => this._channel.socksData(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksError, (payload: socks.SocksSocketErrorPayload) => this._channel.socksError(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksFailed, (payload: socks.SocksSocketFailedPayload) => this._channel.socksFailed(payload));
this._handler.on(socks.SocksProxyHandler.Events.SocksEnd, (payload: socks.SocksSocketEndPayload) => this._channel.socksEnd(payload));
this._channel.on('socksRequested', payload => this._handler.socketRequested(payload));
this._channel.on('socksClosed', payload => this._handler.socketClosed(payload));
this._channel.on('socksData', payload => this._handler.sendSocketData(payload));
}
cleanup() {
this._handler.cleanup();
}
interceptMessage(message: any): boolean {
if (this._ids.has(message.id)) {
this._ids.delete(message.id);
return true;
}
if (message.guid === this._socksSupportObjectGuid) {
const validator = findValidator('SocksSupport', message.method, 'Event');
const params = validator(message.params, '', { tChannelImpl: tChannelForSocks, binary: 'fromBase64' });
this._channel.emit(message.method, params);
return true;
}
return false;
}
}
function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): number {
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
let matches = 0;
@ -272,3 +383,37 @@ function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray):
return matches;
}
function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
throw new ValidationError(`${path}: channels are not expected in SocksSupport`);
}
async function urlToWSEndpoint(progress: Progress, endpointURL: string): Promise<string> {
if (endpointURL.startsWith('ws'))
return endpointURL;
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
const fetchUrl = new URL(endpointURL);
if (!fetchUrl.pathname.endsWith('/'))
fetchUrl.pathname += '/';
fetchUrl.pathname += 'json';
const json = await fetchData({
url: fetchUrl.toString(),
method: 'GET',
timeout: progress.timeUntilDeadline(),
headers: { 'User-Agent': getUserAgent() },
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
`This does not look like a Playwright server, try connecting via ws://.`);
});
progress.throwIfAborted();
const wsUrl = new URL(endpointURL);
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
if (wsEndpointPath.startsWith('/'))
wsEndpointPath = wsEndpointPath.substring(1);
if (!wsUrl.pathname.endsWith('/'))
wsUrl.pathname += '/';
wsUrl.pathname += wsEndpointPath;
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
return wsUrl.toString();
}

View File

@ -47,7 +47,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
android: new AndroidDispatcher(scope, playwright.android),
electron: new ElectronDispatcher(scope, playwright.electron),
utils: new LocalUtilsDispatcher(scope),
utils: new LocalUtilsDispatcher(scope, playwright),
deviceDescriptors,
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
preLaunchedBrowser: browserDispatcher,

View File

@ -43,7 +43,7 @@ export class SdkObject extends EventEmitter {
attribution: Attribution;
instrumentation: Instrumentation;
protected constructor(parent: SdkObject, guidPrefix?: string, guid?: string) {
constructor(parent: SdkObject, guidPrefix?: string, guid?: string) {
super();
this.guid = guid || `${guidPrefix || ''}@${createGuid()}`;
this.setMaxListeners(0);

View File

@ -389,6 +389,7 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
harLookup(params: LocalUtilsHarLookupParams, metadata?: Metadata): Promise<LocalUtilsHarLookupResult>;
harClose(params: LocalUtilsHarCloseParams, metadata?: Metadata): Promise<LocalUtilsHarCloseResult>;
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: Metadata): Promise<LocalUtilsHarUnzipResult>;
connect(params: LocalUtilsConnectParams, metadata?: Metadata): Promise<LocalUtilsConnectResult>;
}
export type LocalUtilsZipParams = {
zipFile: string,
@ -442,6 +443,22 @@ export type LocalUtilsHarUnzipOptions = {
};
export type LocalUtilsHarUnzipResult = void;
export type LocalUtilsConnectParams = {
wsEndpoint: string,
headers?: any,
slowMo?: number,
timeout?: number,
socksProxyRedirectPortForTest?: number,
};
export type LocalUtilsConnectOptions = {
headers?: any,
slowMo?: number,
timeout?: number,
socksProxyRedirectPortForTest?: number,
};
export type LocalUtilsConnectResult = {
pipe: JsonPipeChannel,
};
export interface LocalUtilsEvents {
}
@ -765,27 +782,10 @@ export interface BrowserTypeEventTarget {
}
export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel {
_type_BrowserType: boolean;
connect(params: BrowserTypeConnectParams, metadata?: Metadata): Promise<BrowserTypeConnectResult>;
launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: Metadata): Promise<BrowserTypeConnectOverCDPResult>;
}
export type BrowserTypeConnectParams = {
wsEndpoint: string,
headers?: any,
slowMo?: number,
timeout?: number,
socksProxyRedirectPortForTest?: number,
};
export type BrowserTypeConnectOptions = {
headers?: any,
slowMo?: number,
timeout?: number,
socksProxyRedirectPortForTest?: number,
};
export type BrowserTypeConnectResult = {
pipe: JsonPipeChannel,
};
export type BrowserTypeLaunchParams = {
channel?: string,
executablePath?: string,

View File

@ -528,6 +528,16 @@ LocalUtils:
zipFile: string
harFile: string
connect:
parameters:
wsEndpoint: string
headers: json?
slowMo: number?
timeout: number?
socksProxyRedirectPortForTest: number?
returns:
pipe: JsonPipe
Root:
type: interface
@ -772,16 +782,6 @@ BrowserType:
commands:
connect:
parameters:
wsEndpoint: string
headers: json?
slowMo: number?
timeout: number?
socksProxyRedirectPortForTest: number?
returns:
pipe: JsonPipe
launch:
parameters:
$mixin: LaunchOptions