Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
soundofspace committed Jan 8, 2025
1 parent 19de3ad commit bb8ad29
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 259 deletions.
4 changes: 1 addition & 3 deletions agent/main/lib/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
if (options.proxyPort !== undefined && !launchArgs.some(x => x.startsWith('--proxy-server'))) {
launchArgs.push(
// Use proxy for localhost URLs
'--proxy-bypass-list=<-loopback>;websocket.localhost',
'--proxy-bypass-list=<-loopback>;agent.localhost',
`--proxy-server=localhost:${options.proxyPort}`,
);
}
Expand Down Expand Up @@ -461,7 +461,6 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
});
}
const context = new BrowserContext(this, false);
void context.initialize();
context.hooks = this.browserContextCreationHooks ?? {};
context.id = targetInfo.browserContextId;
context.targetsById.set(targetInfo.targetId, targetInfo);
Expand Down Expand Up @@ -573,7 +572,6 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
private async onNewContext(context: BrowserContext): Promise<void> {
const id = context.id;
this.browserContextsById.set(id, context);
await context.initialize();
context.once('close', () => this.browserContextsById.delete(id));
this.emit('new-context', { context });
await this.hooks?.onNewBrowserContext?.(context);
Expand Down
10 changes: 1 addition & 9 deletions agent/main/lib/BrowserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import WebsocketMessages from './WebsocketMessages';
import { DefaultCommandMarker } from './DefaultCommandMarker';
import DevtoolsSessionLogger from './DevtoolsSessionLogger';
import FrameOutOfProcess from './FrameOutOfProcess';
import { WebsocketSession } from './WebsocketSession';
import CookieParam = Protocol.Network.CookieParam;
import TargetInfo = Protocol.Target.TargetInfo;
import CreateBrowserContextRequest = Protocol.Target.CreateBrowserContextRequest;
Expand Down Expand Up @@ -69,7 +68,6 @@ export default class BrowserContext
}

public isIncognito = true;
public readonly websocketSession: WebsocketSession;

public readonly idTracker = {
navigationId: 0,
Expand Down Expand Up @@ -104,11 +102,6 @@ export default class BrowserContext
this.devtoolsSessionLogger.subscribeToDevtoolsMessages(this.browser.devtoolsSession, {
sessionType: 'browser',
});
this.websocketSession = new WebsocketSession();
}

public async initialize(): Promise<void> {
await this.websocketSession.initialize();
}

public async open(): Promise<void> {
Expand All @@ -118,7 +111,7 @@ export default class BrowserContext
disposeOnDetach: true,
};
if (this.proxy?.address) {
createContextOptions.proxyBypassList = '<-loopback>;websocket.localhost';
createContextOptions.proxyBypassList = '<-loopback>;agent.localhost';
createContextOptions.proxyServer = this.proxy.address;
}

Expand Down Expand Up @@ -351,7 +344,6 @@ export default class BrowserContext
this.resources.cleanup();
this.events.close();
this.emit('close');
this.websocketSession.close();
this.devtoolsSessionLogger.close();
this.removeAllListeners();
this.cleanup();
Expand Down
146 changes: 146 additions & 0 deletions agent/main/lib/Console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Currently this only used to support communication from chrome (injected scripts) to unblocked agent

import Log from '@ulixee/commons/lib/Logger';
import Resolvable from '@ulixee/commons/lib/Resolvable';
import TypedEventEmitter from '@ulixee/commons/lib/TypedEventEmitter';
import { Server } from 'net';
import DevtoolsSession from './DevtoolsSession';
import EventSubscriber from '@ulixee/commons/lib/EventSubscriber';
import Protocol from 'devtools-protocol';
import { IConsoleEvents } from '@ulixee/unblocked-specification/agent/browser/IConsole';

const SCRIPT_PLACEHOLDER = '';
const { log } = Log(module);

Check warning on line 13 in agent/main/lib/Console.ts

View workflow job for this annotation

GitHub Actions / Test chrome-latest (node 18; ubuntu-latest)

'log' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 13 in agent/main/lib/Console.ts

View workflow job for this annotation

GitHub Actions / Test chrome-latest (node 18; ubuntu-latest)

'log' is assigned a value but never used. Allowed unused vars must match /^_/u

export class Console extends TypedEventEmitter<IConsoleEvents> {
isReady: Resolvable<void>;

private readonly host = 'agent.localhost';
private port = 9999;

private readonly secret = Math.random().toString();
private readonly events = new EventSubscriber();
// We store resolvable when we received websocket message before, receiving
// targetId, this way we can await this, and still trigger to get proper ids.
private clientIdToTargetId = new Map<string, Resolvable<string> | string>();

private server: Server;
private intervals = new Set<NodeJS.Timeout>();

constructor(public devtoolsSession: DevtoolsSession) {
super();
}

async initialize(): Promise<void> {
if (this.isReady) return this.isReady.promise;
this.isReady = new Resolvable();

await this.devtoolsSession.send('Console.enable');
this.events.on(
this.devtoolsSession,
'Console.messageAdded',
this.handleConsoleMessage.bind(this),
);

this.isReady.resolve();
return this.isReady.promise;
}

isConsoleRegisterUrl(url: string): boolean {
try {
const parsed = new URL(url);
return (
parsed.hostname === this.host
// parsed.port === this.port.toString() &&
// parsed.searchParams.get('secret') === this.secret
);
} catch {
return false;
}
}

registerFrameId(url: string, frameId: string): void {
const parsed = new URL(url);
if (parsed.searchParams.get('secret') !== this.secret) return;
const clientId = parsed.searchParams.get('clientId');
if (!clientId) return;

const targetId = this.clientIdToTargetId.get(clientId);
if (targetId instanceof Resolvable) {
targetId.resolve(frameId);
}
this.clientIdToTargetId.set(clientId, frameId);
}

injectCallbackIntoScript(script: string): string {
// We could do this as a simple template script but this logic might get
// complex over time and we want typescript to be able to check proxyScript();
const scriptFn = injectedScript
.toString()
// eslint-disable-next-line no-template-curly-in-string
.replaceAll('${this.host}', this.host)
// eslint-disable-next-line no-template-curly-in-string
.replaceAll('${this.port}', this.port.toString())
// eslint-disable-next-line no-template-curly-in-string
.replaceAll('${this.secret}', this.secret)
// Use function otherwise replace will try todo some magic
.replace('SCRIPT_PLACEHOLDER', () => script);

const wsScript = `(${scriptFn})();`;
return wsScript;
}

private async handleConsoleMessage(msgAdded: Protocol.Console.MessageAddedEvent): Promise<void> {
if (msgAdded.message.source !== 'console-api' || msgAdded.message.level !== 'debug') return;

let clientId: string;
let name: string;
let payload: any;

try {
// Doing this is much much cheaper than json parse on everything logged in console debug
const [secret, maybeClientId, serializedData] = msgAdded.message.text.split('-_-');
if (secret !== this.secret) return;

const data = JSON.parse(serializedData);
name = data.name;
payload = data.payload;
clientId = maybeClientId;
} catch {
return;
}

let frameId = this.clientIdToTargetId.get(clientId);
if (!frameId) {
const resolvable = new Resolvable<string>();
this.clientIdToTargetId.set(clientId, resolvable);
frameId = await resolvable.promise;
} else if (frameId instanceof Resolvable) {
frameId = await frameId.promise;
}

this.emit('callback-received', { id: frameId, name, payload });
}
}

/** This function will be stringified and inserted as a wrapper script so all injected
* scripts have access to a callback function (over a websocket). This function takes
* care of setting up that websocket and all other logic it needs as glue to make it all work.
* */
function injectedScript(): void {
const clientId = Math.random();
const url = `${this.host}:${this.port}?secret=${this.secret}&clientId=${clientId}`;
// This will signal to network manager we are trying to make websocket connection
// This is needed later to map clientId to frameId
void fetch(`http://${url}`, { mode: 'no-cors' }).catch(() => undefined);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const callback = (name, payload): void => {
const serializedData = JSON.stringify({ name, payload });
// eslint-disable-next-line no-console
console.debug(`${this.secret}-_-${clientId}-_-${serializedData}`);
};

// eslint-disable-next-line @typescript-eslint/no-unused-expressions
SCRIPT_PLACEHOLDER;
}
6 changes: 0 additions & 6 deletions agent/main/lib/DevtoolsSessionLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,6 @@ export default class DevtoolsSessionLogger extends TypedEventEmitter<IDevtoolsLo
params.networkId ??
params.requestId;
if (params.networkId) this.fetchRequestIdToNetworkId.set(params.requestId, params.networkId);
if (
event.method === 'Network.webSocketCreated' &&
this.browserContext.websocketSession?.isWebsocketUrl(params.url)
) {
this.requestsToSkip.add(requestId);
}

if (!pageId && params.targetInfo && params.targetInfo?.type === 'page') {
pageId = params.targetInfo.targetId;
Expand Down
1 change: 0 additions & 1 deletion agent/main/lib/FrameOutOfProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export default class FrameOutOfProcess {
this.frame = frame;
this.networkManager = new NetworkManager(
this.devtoolsSession,
this.browserContext.websocketSession,
frame.logger,
page.browserContext.proxy,
);
Expand Down
32 changes: 19 additions & 13 deletions agent/main/lib/FramesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import DomStorageTracker from './DomStorageTracker';
import InjectedScripts from './InjectedScripts';
import Page from './Page';
import Resources from './Resources';
import { WebsocketSession } from './WebsocketSession';
import FrameNavigatedEvent = Protocol.Page.FrameNavigatedEvent;
import FrameTree = Protocol.Page.FrameTree;
import FrameDetachedEvent = Protocol.Page.FrameDetachedEvent;
Expand All @@ -29,6 +28,9 @@ import FrameStoppedLoadingEvent = Protocol.Page.FrameStoppedLoadingEvent;
import LifecycleEventEvent = Protocol.Page.LifecycleEventEvent;
import FrameRequestedNavigationEvent = Protocol.Page.FrameRequestedNavigationEvent;
import TargetInfo = Protocol.Target.TargetInfo;
import { Console } from './Console';
import { IBrowserNetworkEvents } from '@ulixee/unblocked-specification/agent/browser/IBrowserNetworkEvents';
import { IConsoleEvents } from '@ulixee/unblocked-specification/agent/browser/IConsole';

export const DEFAULT_PAGE = 'about:blank';
export const ISOLATED_WORLD = '__agent_world__';
Expand Down Expand Up @@ -63,16 +65,14 @@ export default class FramesManager extends TypedEventEmitter<IFrameManagerEvents
return this.page.browserContext.resources;
}

private get websocketSession(): WebsocketSession {
return this.page.browserContext.websocketSession;
}

private attachedFrameIds = new Set<string>();
private readonly events = new EventSubscriber();
private readonly networkManager: NetworkManager;
private readonly domStorageTracker: DomStorageTracker;
private pageCallbacks = new Map<string, TNewDocumentCallbackFn>();

private console: Console;

private isReady: Promise<void>;

constructor(page: Page, devtoolsSession: DevtoolsSession) {
Expand All @@ -85,16 +85,21 @@ export default class FramesManager extends TypedEventEmitter<IFrameManagerEvents

bindFunctions(this);

this.console = new Console(devtoolsSession);

this.events.on(page, 'resource-will-be-requested', this.onResourceWillBeRequested);
this.events.on(page, 'resource-was-requested', this.onResourceWasRequested);
this.events.on(page, 'resource-loaded', this.onResourceLoaded);
this.events.on(page, 'resource-failed', this.onResourceFailed);
this.events.on(page, 'navigation-response', this.onNavigationResourceResponse);
this.events.on(
this.websocketSession,
'message-received',
this.onWebsocketSessionMessageReceived,
);
this.events.on(this.console, 'callback-received', this.onCallbackReceived);

this.events.on(this.networkManager, 'internal-request', (event: IBrowserNetworkEvents['internal-request']) => {
const url = event.request.request.url;
if (this.console.isConsoleRegisterUrl(url)) {
this.console.registerFrameId(url, event.request.frameId);
}
});
}

public initialize(devtoolsSession: DevtoolsSession): Promise<void> {
Expand Down Expand Up @@ -153,6 +158,7 @@ export default class FramesManager extends TypedEventEmitter<IFrameManagerEvents
}),
devtoolsSession.send('Page.setLifecycleEventsEnabled', { enabled: true }),
InjectedScripts.install(this, devtoolsSession, this.onDomPaintEvent),
this.console.initialize(),
]);
this.recurseFrameTree(devtoolsSession, framesResponse.frameTree);
resolve();
Expand Down Expand Up @@ -209,7 +215,7 @@ export default class FramesManager extends TypedEventEmitter<IFrameManagerEvents
): Promise<{ identifier: string }> {
devtoolsSession ??= this.devtoolsSession;
if (callbacks) {
script = this.websocketSession.injectWebsocketCallbackIntoScript(script);
script = this.console.injectCallbackIntoScript(script);
for (const [name, onCallbackFn] of Object.entries(callbacks)) {
if (onCallbackFn) {
if (this.pageCallbacks.has(name) && this.pageCallbacks.get(name) !== onCallbackFn)
Expand Down Expand Up @@ -691,8 +697,8 @@ export default class FramesManager extends TypedEventEmitter<IFrameManagerEvents
);
}

private async onWebsocketSessionMessageReceived(
event: IWebsocketEvents['message-received'],
private async onCallbackReceived(
event: IConsoleEvents['callback-received'],
): Promise<void> {
const callback = this.pageCallbacks.get(event.name);
let frame = this.framesById.get(event.id);
Expand Down
14 changes: 3 additions & 11 deletions agent/main/lib/NetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import LoadingFailedEvent = Protocol.Network.LoadingFailedEvent;
import RequestServedFromCacheEvent = Protocol.Network.RequestServedFromCacheEvent;
import RequestWillBeSentExtraInfoEvent = Protocol.Network.RequestWillBeSentExtraInfoEvent;
import IProxyConnectionOptions from '../interfaces/IProxyConnectionOptions';
import { WebsocketSession } from './WebsocketSession';

interface IResourcePublishing {
hasRequestWillBeSentEvent: boolean;
Expand All @@ -37,7 +36,6 @@ interface IResourcePublishing {
const mbBytes = 1028 * 1028;

export default class NetworkManager extends TypedEventEmitter<IBrowserNetworkEvents> {
public readonly websocketSession: WebsocketSession;
protected readonly logger: IBoundLog;
private readonly devtools: DevtoolsSession;
private readonly attemptedAuthentications = new Set<string>();
Expand All @@ -62,13 +60,11 @@ export default class NetworkManager extends TypedEventEmitter<IBrowserNetworkEve

constructor(
devtoolsSession: DevtoolsSession,
websocketSession: WebsocketSession,
logger: IBoundLog,
proxyConnectionOptions?: IProxyConnectionOptions,
) {
super();
this.devtools = devtoolsSession;
this.websocketSession = websocketSession;
this.logger = logger.createChild(module);
this.proxyConnectionOptions = proxyConnectionOptions;
bindFunctions(this);
Expand Down Expand Up @@ -308,8 +304,8 @@ export default class NetworkManager extends TypedEventEmitter<IBrowserNetworkEve
if (this.requestIdsToIgnore.has(networkRequest.requestId)) return;

const url = networkRequest.request.url;
if (this.websocketSession.isWebsocketUrl(url)) {
this.websocketSession.registerWebsocketFrameId(url, networkRequest.frameId);
if (url.includes('agent.localhost')) {
this.emit('internal-request', { request: networkRequest });
this.addRequestIdToIgnore(networkRequest.requestId);
return;
}
Expand Down Expand Up @@ -630,11 +626,7 @@ export default class NetworkManager extends TypedEventEmitter<IBrowserNetworkEve
}
/////// WEBSOCKET EVENT HANDLERS /////////////////////////////////////////////////////////////////

private onWebSocketCreated(event: WebSocketCreatedEvent): void {
if (this.websocketSession.isWebsocketUrl(event.url)) {
this.addRequestIdToIgnore(event.requestId);
}
}
private onWebSocketCreated(_event: WebSocketCreatedEvent): void {}

private onWebsocketHandshake(handshake: WebSocketWillSendHandshakeRequestEvent): void {
if (this.requestIdsToIgnore.has(handshake.requestId)) return;
Expand Down
1 change: 0 additions & 1 deletion agent/main/lib/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ export default class Page extends TypedEventEmitter<IPageLevelEvents> implements
this.mouse = new Mouse(devtoolsSession, this.keyboard);
this.networkManager = new NetworkManager(
devtoolsSession,
this.browserContext.websocketSession,
this.logger,
this.browserContext.proxy,
);
Expand Down
Loading

0 comments on commit bb8ad29

Please sign in to comment.