playwright/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts
2025-02-12 14:43:52 -08:00

156 lines
6.5 KiB
TypeScript

/**
* 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 } from './dispatcher';
import { SdkObject } from '../../server/instrumentation';
import * as localUtils from '../../utils/localUtils';
import { nodePlatform } from '../../utils/platform';
import { getUserAgent } from '../../utils/userAgent';
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import { Progress, ProgressController } from '../progress';
import { SocksInterceptor } from '../socksInterceptor';
import { WebSocketTransport } from '../transport';
import { fetchData } from '../utils/network';
import type { HarBackend } from '../../utils/harBackend';
import type { CallMetadata } from '../instrumentation';
import type { Playwright } from '../playwright';
import type { RootDispatcher } from './dispatcher';
import type * as channels from '@protocol/channels';
import type * as http from 'http';
import type { HTTPRequestParams } from '../utils/network';
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
_type_LocalUtils: boolean;
private _harBackends = new Map<string, HarBackend>();
private _stackSessions = new Map<string, localUtils.StackSession>();
constructor(scope: RootDispatcher, playwright: Playwright) {
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
const deviceDescriptors = Object.entries(descriptors)
.map(([name, descriptor]) => ({ name, descriptor }));
super(scope, localUtils, 'LocalUtils', {
deviceDescriptors,
});
this._type_LocalUtils = true;
}
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
return await localUtils.zip(nodePlatform, this._stackSessions, params);
}
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
return await localUtils.harOpen(nodePlatform, this._harBackends, params);
}
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
return await localUtils.harLookup(this._harBackends, params);
}
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
return await localUtils.harClose(this._harBackends, params);
}
async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata: CallMetadata): Promise<void> {
return await localUtils.harUnzip(params);
}
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
return await localUtils.tracingStarted(this._stackSessions, params);
}
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
return await localUtils.traceDiscarded(nodePlatform, this._stackSessions, params);
}
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
}
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 wsHeaders = {
'User-Agent': getUserAgent(),
'x-playwright-proxy': params.exposeNetwork ?? '',
...params.headers,
};
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
const transport = await WebSocketTransport.connect(progress, wsEndpoint, wsHeaders, true, 'x-playwright-debug-log');
const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest);
const pipe = new JsonPipeDispatcher(this);
transport.onmessage = json => {
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 = (reason?: string) => {
socksInterceptor?.cleanup();
pipe.wasClosed(reason);
};
pipe.on('close', () => transport.close());
return { pipe, headers: transport.headers };
}, params.timeout || 0);
}
}
async function urlToWSEndpoint(progress: Progress | undefined, 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() ?? 30_000,
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();
}