move under own dispatcher

This commit is contained in:
Simon Knott 2025-01-23 15:03:53 +01:00
parent 048f6d95cf
commit 10646b1c25
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
8 changed files with 114 additions and 229 deletions

View file

@ -66,7 +66,6 @@ export const slowMoActions = new Set([
export const commandsWithTracingSnapshots = new Set([
'EventTarget.waitForEventInfo',
'LocalUtils.waitForEventInfo',
'MockingProxy.waitForEventInfo',
'BrowserContext.waitForEventInfo',
'Page.waitForEventInfo',

View file

@ -236,7 +236,6 @@ scheme.EventTargetWaitForEventInfoParams = tObject({
error: tOptional(tString),
}),
});
scheme.LocalUtilsWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.MockingProxyWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.BrowserContextWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.PageWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
@ -244,7 +243,6 @@ scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParam
scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.EventTargetWaitForEventInfoResult = tOptional(tObject({}));
scheme.LocalUtilsWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
scheme.MockingProxyWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
scheme.BrowserContextWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');
scheme.PageWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult');

View file

@ -20,7 +20,7 @@ import path from 'path';
import os from 'os';
import type * as channels from '@protocol/channels';
import { ManualPromise } from '../../utils/manualPromise';
import { assert, calculateSha1, createGuid, HttpServer, removeFolders, urlMatches } from '../../utils';
import { assert, calculateSha1, createGuid, removeFolders } from '../../utils';
import type { RootDispatcher } from './dispatcher';
import { Dispatcher } from './dispatcher';
import { yazl, yauzl } from '../../zipBundle';
@ -41,14 +41,13 @@ import type { Playwright } from '../playwright';
import { SdkObject } from '../../server/instrumentation';
import { serializeClientSideCallMetadata } from '../../utils';
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
import { APIRequestContextDispatcher, RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers';
import type { APIRequestContext } from '../fetch';
import { GlobalAPIRequestContext } from '../fetch';
import { MockingProxy, ServerInterceptionRegistry } from '../mockingProxy';
import { MockingProxy } from '../mockingProxy';
import { MockingProxyDispatcher } from './mockingProxyDispatcher';
export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
_type_LocalUtils: boolean;
_type_EventTarget: boolean;
private _harBackends = new Map<string, HarBackend>();
private _stackSessions = new Map<string, {
file: string,
@ -56,9 +55,7 @@ export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUt
tmpDir: string | undefined,
callStacks: channels.ClientSideCallMetadata[]
}>();
_requestContext: APIRequestContext;
private _interceptionRegistry;
private _server?: WorkerHttpServer;
private _requestContext: APIRequestContext;
constructor(scope: RootDispatcher, playwright: Playwright) {
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
@ -68,40 +65,9 @@ export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUt
const requestContext = new GlobalAPIRequestContext(playwright, {});
super(scope, localUtils, 'LocalUtils', {
deviceDescriptors,
requestContext: APIRequestContextDispatcher.from(scope, requestContext),
});
this._requestContext = requestContext;
this._type_LocalUtils = true;
this._type_EventTarget = true;
this._interceptionRegistry = new ServerInterceptionRegistry(this._object, this._requestContext, {
onRequest: request => {
this._dispatchEvent('request', { request: RequestDispatcher.from(this.parentScope() as any, request) });
},
onRequestFinished: (request, response) => {
this._dispatchEvent('requestFinished', {
request: RequestDispatcher.from(this.parentScope() as any, request),
response: ResponseDispatcher.fromNullable(this.parentScope() as any, response ?? null),
responseEndTiming: request._responseEndTiming,
});
},
onRequestFailed: request => {
this._dispatchEvent('requestFailed', {
request: RequestDispatcher.from(this.parentScope() as any, request),
responseEndTiming: request._responseEndTiming,
failureText: request._failureText ?? undefined
});
},
onResponse: (request, response) => {
this._dispatchEvent('response', {
request: RequestDispatcher.from(this.parentScope() as any, request),
response: ResponseDispatcher.from(this.parentScope() as any, response),
});
},
onRoute: (route, request) => {
this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this.parentScope() as any, request), route) });
},
});
}
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
@ -316,24 +282,10 @@ export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUt
this._stackSessions.delete(stacksId!);
}
async setServerNetworkInterceptionPatterns(params: channels.LocalUtilsSetServerNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<channels.LocalUtilsSetServerNetworkInterceptionPatternsResult> {
if (!this._server) {
this._server = new WorkerHttpServer();
new MockingProxy(this._interceptionRegistry).install(this._server);
await this._server.start({ port: params.port });
}
if (params.patterns.length === 0)
return this._interceptionRegistry.setRequestInterceptor(undefined);
const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
this._interceptionRegistry.setRequestInterceptor(url => urlMatchers.some(urlMatch => urlMatches(undefined, url, urlMatch)));
}
}
export class WorkerHttpServer extends HttpServer {
override _handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
return false;
async newMockingProxy(params: channels.LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise<channels.LocalUtilsNewMockingProxyResult> {
const mockingProxy = new MockingProxy(this._object, this._requestContext);
await mockingProxy.start(params.port);
return { mockingProxy: MockingProxyDispatcher.from(this.parentScope(), mockingProxy) };
}
}

View file

@ -14,27 +14,33 @@
* limitations under the License.
*/
import type { CallMetadata } from '@protocol/callMetadata';
import type { MockingProxy, ServerInterceptionRegistry } from '../mockingProxy';
import type { MockingProxy } from '../mockingProxy';
import type { RootDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
import type * as channels from '@protocol/channels';
import { SdkObject } from '../instrumentation';
import { APIRequestContextDispatcher } from './networkDispatchers';
import { urlMatches } from '@isomorphic/urlMatch';
export class MockingProxyDispatcher extends Dispatcher<ServerInterceptionRegistry, channels.MockingProxyChannel, RootDispatcher> implements channels.MockingProxyChannel {
export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.MockingProxyChannel, RootDispatcher> implements channels.MockingProxyChannel {
_type_MockingProxy = true;
_type_EventTarget = true;
static from(scope: RootDispatcher, mockingProxy: ServerInterceptionRegistry): MockingProxyDispatcher {
static from(scope: RootDispatcher, mockingProxy: MockingProxy): MockingProxyDispatcher {
return existingDispatcher<MockingProxyDispatcher>(mockingProxy) || new MockingProxyDispatcher(scope, mockingProxy);
}
private constructor(scope: RootDispatcher, mockingProxy: ServerInterceptionRegistry) {
private constructor(scope: RootDispatcher, mockingProxy: MockingProxy) {
super(scope, mockingProxy, 'MockingProxy', {
port: mockingProxy.port(),
port: mockingProxy.port,
requestContext: APIRequestContextDispatcher.from(scope, mockingProxy.fetchRequest),
});
}
setInterceptionPatterns(params: channels.MockingProxySetInterceptionPatternsParams, metadata?: CallMetadata): Promise<channels.MockingProxySetInterceptionPatternsResult> {
throw new Error('Method not implemented.');
async setInterceptionPatterns(params: channels.MockingProxySetInterceptionPatternsParams, metadata?: CallMetadata): Promise<channels.MockingProxySetInterceptionPatternsResult> {
if (params.patterns.length === 0)
return this._object.setInterceptionPatterns(undefined);
const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
this._object.setInterceptionPatterns(url => urlMatchers.some(urlMatch => urlMatches(undefined, url, urlMatch)));
}
}

View file

@ -19,144 +19,45 @@ import https from 'https';
import url from 'url';
import type { APIRequestContext } from './fetch';
import { SdkObject } from './instrumentation';
import type { RemoteAddr, RequestContext, ResourceTiming, SecurityDetails } from './network';
import type { RequestContext, ResourceTiming, SecurityDetails } from './network';
import { Request, Response, Route } from './network';
import type { HeadersArray, NormalizedContinueOverrides, NormalizedFulfillResponse } from './types';
import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
import type { WorkerHttpServer } from './dispatchers/localUtilsDispatcher';
import type { HeadersArray, } from './types';
import { HttpServer, ManualPromise, monotonicTime } from '../utils';
import { TLSSocket } from 'tls';
import type { AddressInfo } from 'net';
import { pipeline } from 'stream/promises';
import { Transform } from 'stream';
type InterceptorResult =
| { result: 'continue', request: Request, overrides?: NormalizedContinueOverrides }
| { result: 'abort', request: Request, errorCode: string }
| { result: 'fulfill', request: Request, response: NormalizedFulfillResponse };
interface EventDelegate {
onRequest(request: Request): void;
onRequestFinished(request: Request, response: Response): void;
onRequestFailed(request: Request): void;
onResponse(request: Request, response: Response): void;
onRoute(route: Route, request: Request): void;
}
export class ServerInterceptionRegistry extends SdkObject implements RequestContext {
private _eventDelegate: EventDelegate;
export class MockingProxy extends SdkObject implements RequestContext {
fetchRequest: APIRequestContext;
private _matches?: (url: string) => boolean;
private _httpServer = new WorkerHttpServer();
constructor(parent: SdkObject, requestContext: APIRequestContext, eventDelegate: EventDelegate) {
super(parent, 'serverInterceptionRegistry');
this._eventDelegate = eventDelegate;
constructor(parent: SdkObject, requestContext: APIRequestContext) {
super(parent, 'MockingProxy');
this.fetchRequest = requestContext;
}
setRequestInterceptor(matches?: (url: string) => boolean) {
this._matches = matches;
}
handle(url: string, method: string, body: Buffer | null, headers: HeadersArray): Promise<InterceptorResult> {
const request = new Request(this, null, null, null, undefined, url, '', method, body, headers);
request.setRawRequestHeaders(headers);
this._eventDelegate.onRequest(request);
if (!this._matches?.(url))
return Promise.resolve({ result: 'continue', request });
return new Promise(resolve => {
const route = new Route(request, {
async abort(errorCode) {
resolve({ result: 'abort', request, errorCode });
},
async continue(overrides) {
resolve({ result: 'continue', request, overrides });
},
async fulfill(response) {
resolve({ result: 'fulfill', request, response });
},
});
this._eventDelegate.onRoute(route, request);
});
}
failed(request: Request, error: string) {
request._setFailureText(error);
this._eventDelegate.onRequestFailed(request);
}
response(request: Request, status: number, statusText: string, headers: HeadersArray, body: () => Promise<Buffer>, httpVersion: string, timing: ResourceTiming, securityDetails: SecurityDetails | undefined, serverAddr: RemoteAddr | undefined) {
const response = new Response(request, status, statusText, headers, timing, body, false, httpVersion);
response.setRawResponseHeaders(headers);
response._securityDetailsFinished(securityDetails);
response._serverAddrFinished(serverAddr);
this._eventDelegate.onResponse(request, response);
return {
finished: async (responseEndTiming: number, transferSize: number, encodedBodySize: number) => {
response._requestFinished(responseEndTiming);
response.setTransferSize(transferSize);
response.setEncodedBodySize(encodedBodySize);
response.setResponseHeadersSize(transferSize - encodedBodySize);
this._eventDelegate.onRequestFinished(request, response);
}
};
}
addRouteInFlight(route: Route): void {
}
removeRouteInFlight(route: Route): void {
}
}
function headersArray(req: Pick<http.IncomingMessage, 'headersDistinct'>): HeadersArray {
return Object.entries(req.headersDistinct).flatMap(([name, values = []]) => values.map(value => ({ name, value })));
}
function headersArrayToOutgoingHeaders(headers: HeadersArray) {
const result: http.OutgoingHttpHeaders = {};
for (const { name, value } of headers) {
if (result[name] === undefined)
result[name] = value;
else if (Array.isArray(result[name]))
result[name].push(value);
else
result[name] = [result[name] as string, value];
}
return result;
}
async function collectBody(req: http.IncomingMessage) {
return await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
export class MockingProxy {
private readonly _registry: ServerInterceptionRegistry;
constructor(registry: ServerInterceptionRegistry) {
this._registry = registry;
}
install(server: WorkerHttpServer) {
server.routePrefix('/', (req, res) => {
this._httpServer.routePrefix('/', (req, res) => {
this._proxy(req, res);
return true;
});
server.server().on('connect', (req, socket, head) => {
this._httpServer.server().on('connect', (req, socket, head) => {
socket.end('HTTP/1.1 405 Method Not Allowed\r\n\r\n');
});
}
async start(port?: number): Promise<void> {
await this._httpServer.start({ port });
}
get port() {
return this._httpServer.port();
}
setInterceptionPatterns(matches?: (url: string) => boolean) {
this._matches = matches;
}
private async _proxy(req: http.IncomingMessage, res: http.ServerResponse) {
if (req.url?.startsWith('/'))
req.url = req.url.substring(1);
@ -170,14 +71,14 @@ export class MockingProxy {
delete req.headersDistinct.host;
const headers = headersArray(req);
const body = await collectBody(req);
const result = await this._registry.handle(req.url!, req.method!, body, headers);
switch (result.result) {
case 'abort': {
req.destroy(result.errorCode ? new Error(result.errorCode) : undefined);
return;
}
case 'continue': {
const { overrides } = result;
const request = new Request(this, null, null, null, undefined, req.url!, '', req.method!, body, headers);
request.setRawRequestHeaders(headers);
const route = new Route(request, {
abort: async errorCode => {
req.destroy(errorCode ? new Error(errorCode) : undefined);
},
continue: async overrides => {
const proxyUrl = url.parse(overrides?.url ?? req.url!);
const httpLib = proxyUrl.protocol === 'https:' ? https : http;
const proxyHeaders = overrides?.headers ?? headers;
@ -225,16 +126,11 @@ export class MockingProxy {
const address = socket.address() as AddressInfo;
const responseBodyPromise = new ManualPromise<Buffer>();
const response = this._registry.response(
result.request,
proxyRes.statusCode!,
proxyRes.statusMessage!, headersArray(proxyRes),
() => responseBodyPromise,
proxyRes.httpVersion,
timings,
securityDetails,
{ ipAddress: address.family === 'IPv6' ? `[${address.address}]` : address.address, port: address.port },
);
const response = new Response(request, proxyRes.statusCode!, proxyRes.statusMessage!, headersArray(proxyRes), timings, () => responseBodyPromise, false, proxyRes.httpVersion);
response.setRawResponseHeaders(headers);
response._securityDetailsFinished(securityDetails);
response._serverAddrFinished({ ipAddress: address.family === 'IPv6' ? `[${address.address}]` : address.address, port: address.port });
this.emit('response', response);
try {
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
@ -253,20 +149,24 @@ export class MockingProxy {
const body = Buffer.concat(chunks);
responseBodyPromise.resolve(body);
response.finished(
monotonicTime() - startAt,
socket.bytesRead - socketBytesReadStart,
body.byteLength
);
const transferSize = socket.bytesRead - socketBytesReadStart;
const encodedBodySize = body.byteLength;
response._requestFinished(monotonicTime() - startAt);
response.setTransferSize(transferSize);
response.setEncodedBodySize(encodedBodySize);
response.setResponseHeadersSize(transferSize - encodedBodySize);
this.emit('requestFinished', response);
resolve();
} catch (error) {
this._registry.failed(result.request, error.toString());
request._setFailureText('' + error);
this.emit('failed', request);
resolve();
}
});
proxyReq.on('error', error => {
this._registry.failed(result.request, error.toString());
request._setFailureText('' + error);
this.emit('failed', request);
res.statusCode = 502;
res.end(resolve);
});
@ -283,19 +183,51 @@ export class MockingProxy {
});
proxyReq.end(proxyBody);
});
}
case 'fulfill': {
const { response: { status, headers, body, isBase64 } } = result;
},
fulfill: async ({ status, headers, body, isBase64 }) => {
res.statusCode = status;
for (const { name, value } of headers)
res.appendHeader(name, value);
res.sendDate = false;
res.end(Buffer.from(body, isBase64 ? 'base64' : 'utf-8'));
return;
}
default: {
throw new Error('Unexpected result');
}
}
},
});
if (this._matches?.(req.url!))
this.emit('route', { route, request });
else
await route.continue({ isFallback: false });
}
}
function headersArray(req: Pick<http.IncomingMessage, 'headersDistinct'>): HeadersArray {
return Object.entries(req.headersDistinct).flatMap(([name, values = []]) => values.map(value => ({ name, value })));
}
function headersArrayToOutgoingHeaders(headers: HeadersArray) {
const result: http.OutgoingHttpHeaders = {};
for (const { name, value } of headers) {
if (result[name] === undefined)
result[name] = value;
else if (Array.isArray(result[name]))
result[name].push(value);
else
result[name] = [result[name] as string, value];
}
return result;
}
async function collectBody(req: http.IncomingMessage) {
return await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
export class WorkerHttpServer extends HttpServer {
override _handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
return false;
}
}

View file

@ -89,10 +89,10 @@ export function stripFragmentFromUrl(url: string): string {
}
export interface RequestContext extends SdkObject {
addRouteInFlight(route: Route): void;
removeRouteInFlight(route: Route): void;
fetchRequest: APIRequestContext;
addRouteInFlight?(route: Route): void;
removeRouteInFlight?(route: Route): void;
}
export class Request extends SdkObject {
@ -261,7 +261,7 @@ export class Route extends SdkObject {
super(request._frame || request._context, 'route');
this._request = request;
this._delegate = delegate;
this._request._context.addRouteInFlight(this);
this._request._context.addRouteInFlight?.(this);
}
request(): Request {
@ -347,7 +347,7 @@ export class Route extends SdkObject {
}
private _endHandling() {
this._request._context.removeRouteInFlight(this);
this._request._context.removeRouteInFlight?.(this);
}
}

View file

@ -455,7 +455,7 @@ export type LocalUtilsInitializer = {
};
export interface LocalUtilsEventTarget {
}
export interface LocalUtilsChannel extends LocalUtilsEventTarget, EventTargetChannel {
export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
_type_LocalUtils: boolean;
zip(params: LocalUtilsZipParams, metadata?: CallMetadata): Promise<LocalUtilsZipResult>;
harOpen(params: LocalUtilsHarOpenParams, metadata?: CallMetadata): Promise<LocalUtilsHarOpenResult>;

View file

@ -551,8 +551,6 @@ EventTarget:
LocalUtils:
type: interface
extends: EventTarget
initializer:
deviceDescriptors:
type: array