chore: move connect utility into localUtils (#17590)
This commit is contained in:
parent
3409a37f77
commit
06e73b414f
|
|
@ -143,10 +143,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
|
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
|
||||||
let browser: Browser;
|
let browser: Browser;
|
||||||
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
|
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)
|
if ((params as any).__testHookRedirectPortForwarding)
|
||||||
connectParams.socksProxyRedirectPortForTest = (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 closePipe = () => pipe.close().catch(() => {});
|
||||||
const connection = new Connection(this._connection.localUtils());
|
const connection = new Connection(this._connection.localUtils());
|
||||||
connection.markAsRemote();
|
connection.markAsRemote();
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,16 @@ scheme.LocalUtilsHarUnzipParams = tObject({
|
||||||
harFile: tString,
|
harFile: tString,
|
||||||
});
|
});
|
||||||
scheme.LocalUtilsHarUnzipResult = tOptional(tObject({}));
|
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.RootInitializer = tOptional(tObject({}));
|
||||||
scheme.RootInitializeParams = tObject({
|
scheme.RootInitializeParams = tObject({
|
||||||
sdkLanguage: tString,
|
sdkLanguage: tString,
|
||||||
|
|
@ -414,16 +424,6 @@ scheme.BrowserTypeInitializer = tObject({
|
||||||
executablePath: tString,
|
executablePath: tString,
|
||||||
name: 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({
|
scheme.BrowserTypeLaunchParams = tObject({
|
||||||
channel: tOptional(tString),
|
channel: tOptional(tString),
|
||||||
executablePath: tOptional(tString),
|
executablePath: tOptional(tString),
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,6 @@ import type { RootDispatcher } from './dispatcher';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||||
import type { CallMetadata } from '../instrumentation';
|
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 {
|
export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeChannel, RootDispatcher> implements channels.BrowserTypeChannel {
|
||||||
_type_BrowserType = true;
|
_type_BrowserType = true;
|
||||||
|
|
@ -60,134 +49,4 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
|
||||||
defaultContext: browser._defaultContext ? new BrowserContextDispatcher(browserDispatcher, browser._defaultContext) : undefined,
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ import type * as channels from '@protocol/channels';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import { createGuid } from '../../utils';
|
import { createGuid } from '../../utils';
|
||||||
import { serializeError } from '../../protocol/serializers';
|
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;
|
_type_JsonPipe = true;
|
||||||
constructor(scope: BrowserTypeDispatcher) {
|
constructor(scope: LocalUtilsDispatcher) {
|
||||||
super(scope, { guid: 'jsonPipe@' + createGuid() }, 'JsonPipe', {});
|
super(scope, { guid: 'jsonPipe@' + createGuid() }, 'JsonPipe', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
|
@ -26,17 +26,32 @@ import { yazl, yauzl } from '../../zipBundle';
|
||||||
import { ZipFile } from '../../utils/zipFile';
|
import { ZipFile } from '../../utils/zipFile';
|
||||||
import type * as har from '@trace/har';
|
import type * as har from '@trace/har';
|
||||||
import type { HeadersArray } from '../types';
|
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 {
|
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||||
_type_LocalUtils: boolean;
|
_type_LocalUtils: boolean;
|
||||||
private _harBakends = new Map<string, HarBackend>();
|
private _harBakends = new Map<string, HarBackend>();
|
||||||
|
|
||||||
constructor(scope: RootDispatcher) {
|
constructor(scope: RootDispatcher, playwright: Playwright) {
|
||||||
super(scope, { guid: 'localUtils@' + createGuid() }, 'LocalUtils', {});
|
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
||||||
|
super(scope, localUtils, 'LocalUtils', {});
|
||||||
this._type_LocalUtils = true;
|
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 promise = new ManualPromise<void>();
|
||||||
const zipFile = new yazl.ZipFile();
|
const zipFile = new yazl.ZipFile();
|
||||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
(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;
|
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;
|
let harBackend: HarBackend;
|
||||||
if (params.file.endsWith('.zip')) {
|
if (params.file.endsWith('.zip')) {
|
||||||
const zipFile = new ZipFile(params.file);
|
const zipFile = new ZipFile(params.file);
|
||||||
|
|
@ -110,14 +125,14 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
return { harId: harBackend.id };
|
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);
|
const harBackend = this._harBakends.get(params.harId);
|
||||||
if (!harBackend)
|
if (!harBackend)
|
||||||
return { action: 'error', message: `Internal error: har was not opened` };
|
return { action: 'error', message: `Internal error: har was not opened` };
|
||||||
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
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);
|
const harBackend = this._harBakends.get(params.harId);
|
||||||
if (harBackend) {
|
if (harBackend) {
|
||||||
this._harBakends.delete(harBackend.id);
|
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 dir = path.dirname(params.zipFile);
|
||||||
const zipFile = new ZipFile(params.zipFile);
|
const zipFile = new ZipFile(params.zipFile);
|
||||||
for (const entry of await zipFile.entries()) {
|
for (const entry of await zipFile.entries()) {
|
||||||
|
|
@ -138,6 +153,46 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
zipFile.close();
|
zipFile.close();
|
||||||
await fs.promises.unlink(params.zipFile);
|
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];
|
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 {
|
function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): number {
|
||||||
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
|
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
|
||||||
let matches = 0;
|
let matches = 0;
|
||||||
|
|
@ -272,3 +383,37 @@ function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray):
|
||||||
return matches;
|
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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||||
android: new AndroidDispatcher(scope, playwright.android),
|
android: new AndroidDispatcher(scope, playwright.android),
|
||||||
electron: new ElectronDispatcher(scope, playwright.electron),
|
electron: new ElectronDispatcher(scope, playwright.electron),
|
||||||
utils: new LocalUtilsDispatcher(scope),
|
utils: new LocalUtilsDispatcher(scope, playwright),
|
||||||
deviceDescriptors,
|
deviceDescriptors,
|
||||||
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
|
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
|
||||||
preLaunchedBrowser: browserDispatcher,
|
preLaunchedBrowser: browserDispatcher,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export class SdkObject extends EventEmitter {
|
||||||
attribution: Attribution;
|
attribution: Attribution;
|
||||||
instrumentation: Instrumentation;
|
instrumentation: Instrumentation;
|
||||||
|
|
||||||
protected constructor(parent: SdkObject, guidPrefix?: string, guid?: string) {
|
constructor(parent: SdkObject, guidPrefix?: string, guid?: string) {
|
||||||
super();
|
super();
|
||||||
this.guid = guid || `${guidPrefix || ''}@${createGuid()}`;
|
this.guid = guid || `${guidPrefix || ''}@${createGuid()}`;
|
||||||
this.setMaxListeners(0);
|
this.setMaxListeners(0);
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,7 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
||||||
harLookup(params: LocalUtilsHarLookupParams, metadata?: Metadata): Promise<LocalUtilsHarLookupResult>;
|
harLookup(params: LocalUtilsHarLookupParams, metadata?: Metadata): Promise<LocalUtilsHarLookupResult>;
|
||||||
harClose(params: LocalUtilsHarCloseParams, metadata?: Metadata): Promise<LocalUtilsHarCloseResult>;
|
harClose(params: LocalUtilsHarCloseParams, metadata?: Metadata): Promise<LocalUtilsHarCloseResult>;
|
||||||
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: Metadata): Promise<LocalUtilsHarUnzipResult>;
|
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: Metadata): Promise<LocalUtilsHarUnzipResult>;
|
||||||
|
connect(params: LocalUtilsConnectParams, metadata?: Metadata): Promise<LocalUtilsConnectResult>;
|
||||||
}
|
}
|
||||||
export type LocalUtilsZipParams = {
|
export type LocalUtilsZipParams = {
|
||||||
zipFile: string,
|
zipFile: string,
|
||||||
|
|
@ -442,6 +443,22 @@ export type LocalUtilsHarUnzipOptions = {
|
||||||
|
|
||||||
};
|
};
|
||||||
export type LocalUtilsHarUnzipResult = void;
|
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 {
|
export interface LocalUtilsEvents {
|
||||||
}
|
}
|
||||||
|
|
@ -765,27 +782,10 @@ export interface BrowserTypeEventTarget {
|
||||||
}
|
}
|
||||||
export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel {
|
export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel {
|
||||||
_type_BrowserType: boolean;
|
_type_BrowserType: boolean;
|
||||||
connect(params: BrowserTypeConnectParams, metadata?: Metadata): Promise<BrowserTypeConnectResult>;
|
|
||||||
launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
|
launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
|
||||||
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
|
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
|
||||||
connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: Metadata): Promise<BrowserTypeConnectOverCDPResult>;
|
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 = {
|
export type BrowserTypeLaunchParams = {
|
||||||
channel?: string,
|
channel?: string,
|
||||||
executablePath?: string,
|
executablePath?: string,
|
||||||
|
|
|
||||||
|
|
@ -528,6 +528,16 @@ LocalUtils:
|
||||||
zipFile: string
|
zipFile: string
|
||||||
harFile: string
|
harFile: string
|
||||||
|
|
||||||
|
connect:
|
||||||
|
parameters:
|
||||||
|
wsEndpoint: string
|
||||||
|
headers: json?
|
||||||
|
slowMo: number?
|
||||||
|
timeout: number?
|
||||||
|
socksProxyRedirectPortForTest: number?
|
||||||
|
returns:
|
||||||
|
pipe: JsonPipe
|
||||||
|
|
||||||
Root:
|
Root:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
@ -772,16 +782,6 @@ BrowserType:
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
|
|
||||||
connect:
|
|
||||||
parameters:
|
|
||||||
wsEndpoint: string
|
|
||||||
headers: json?
|
|
||||||
slowMo: number?
|
|
||||||
timeout: number?
|
|
||||||
socksProxyRedirectPortForTest: number?
|
|
||||||
returns:
|
|
||||||
pipe: JsonPipe
|
|
||||||
|
|
||||||
launch:
|
launch:
|
||||||
parameters:
|
parameters:
|
||||||
$mixin: LaunchOptions
|
$mixin: LaunchOptions
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue