Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #104 from soundofspace/feat(browser-emulator)--mor…
Browse files Browse the repository at this point in the history
…e-improvements

Feat(browser-emulator): Migrating away from proxies, lots of extra improvements and enabling TS strict mode
  • Loading branch information
blakebyrnes authored Sep 3, 2024
2 parents afd045b + 5929b9d commit 0162fe3
Show file tree
Hide file tree
Showing 39 changed files with 1,435 additions and 744 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,30 @@ jobs:
os: [macos-13, windows-latest, ubuntu-latest]
node-version: [18, 20]
include:
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-123-0
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-122-0
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-121-0
- node-version: 20.x
os: ubuntu-latest
browser: chrome-120-0
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-119-0
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-118-0
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-117-0
# - node-version: 20.x
# os: ubuntu-latest
# browser: chrome-116-0
- node-version: 20.x
os: ubuntu-latest
browser: chrome-115-0
Expand Down
4 changes: 2 additions & 2 deletions agent/main/lib/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IHooksProvider } from '@ulixee/unblocked-specification/agent/hooks/IHoo
import IEmulationProfile, {
IEmulationOptions,
} from '@ulixee/unblocked-specification/plugin/IEmulationProfile';
import { IUnblockedPluginClass, UnblockedPluginConfig } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import { IUnblockedPluginClass, PluginConfigs } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import { nanoid } from 'nanoid';
import env from '../env';
import ICommandMarker from '../interfaces/ICommandMarker';
Expand All @@ -26,7 +26,7 @@ const { log } = Log(module);
export interface IAgentCreateOptions extends Omit<IEmulationProfile, keyof IEmulationOptions> {
id?: string;
plugins?: IUnblockedPluginClass[];
pluginConfigs?: UnblockedPluginConfig;
pluginConfigs?: PluginConfigs;
commandMarker?: ICommandMarker;
}

Expand Down
5 changes: 4 additions & 1 deletion agent/main/lib/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,11 +356,14 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
private applyDefaultLaunchArgs(options: IBrowserUserConfig): void {
const launchArgs = [
...this.engine.launchArguments,
'--ignore-certificate-errors',
'--no-startup-window',
'--use-mock-keychain', // Use mock keychain on Mac to prevent blocking permissions dialogs
];

if (!options.disableMitm) {
launchArgs.push('--ignore-certificate-errors');
}

if (options.proxyPort !== undefined && !launchArgs.some(x => x.startsWith('--proxy-server'))) {
launchArgs.push(
// Use proxy for localhost URLs
Expand Down
2 changes: 1 addition & 1 deletion agent/main/lib/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export default class Page extends TypedEventEmitter<IPageLevelEvents> implements

evaluate<T>(
expression: string,
options?: { timeoutMs?: number, isolatedFromWebPageEnvironment?: boolean },
options?: { timeoutMs?: number; isolatedFromWebPageEnvironment?: boolean },
): Promise<T> {
return this.mainFrame.evaluate<T>(expression, options);
}
Expand Down
6 changes: 3 additions & 3 deletions agent/main/lib/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import IDevtoolsSession from '@ulixee/unblocked-specification/agent/browser/IDev
import { IHooksProvider } from '@ulixee/unblocked-specification/agent/hooks/IHooks';
import {
IUnblockedPluginClass,
UnblockedPluginConfig,
PluginConfigs,
} from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile';
import IRegisteredEventListener from '@ulixee/commons/interfaces/IRegisteredEventListener';
Expand All @@ -32,7 +32,7 @@ interface ICreatePoolOptions {
certificateStore?: ICertificateStore;
defaultBrowserEngine?: IBrowserEngine;
plugins?: IUnblockedPluginClass[];
pluginConfigs?: UnblockedPluginConfig;
pluginConfigs?: PluginConfigs;
dataDir?: string;
logger?: IBoundLog;
}
Expand All @@ -57,7 +57,7 @@ export default class Pool extends TypedEventEmitter<{
public readonly agentsById = new Map<string, Agent>();
public sharedMitmProxy: MitmProxy;
public plugins: IUnblockedPluginClass[] = [];
public pluginConfigs: UnblockedPluginConfig = {};
public pluginConfigs: PluginConfigs = {};

#activeAgentsCount = 0;
#waitingForAvailability: {
Expand Down
24 changes: 15 additions & 9 deletions agent/main/lib/Worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,20 @@ export class Worker extends TypedEventEmitter<IWorkerEvents> implements IWorker
return this.devtoolsSession.send('Runtime.runIfWaitingForDebugger');
}

return Promise.all([
hooks.onNewWorker(this),
this.devtoolsSession.send('Debugger.enable'),
this.devtoolsSession.send('Debugger.setBreakpointByUrl', {
lineNumber: 0,
url: this.targetInfo.url,
}),
])
const isBlobWorker = this.targetInfo.url.startsWith('blob:');
const promises = [hooks.onNewWorker(this)];

if (!isBlobWorker) {
promises.push(
this.devtoolsSession.send('Debugger.enable'),
this.devtoolsSession.send('Debugger.setBreakpointByUrl', {
lineNumber: 0,
url: this.targetInfo.url,
}),
);
}

return Promise.all(promises)
.then(this.resumeAfterEmulation.bind(this))
.catch(async error => {
if (error instanceof CanceledPromiseError) return;
Expand All @@ -154,8 +160,8 @@ export class Worker extends TypedEventEmitter<IWorkerEvents> implements IWorker

private resumeAfterEmulation(): Promise<any> {
return Promise.all([
this.devtoolsSession.send('Runtime.runIfWaitingForDebugger'),
this.devtoolsSession.send('Debugger.disable'),
this.devtoolsSession.send('Runtime.runIfWaitingForDebugger'),
]);
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"watch": "yarn tsc && tsc -b -w tsconfig.json",
"test": "yarn copy:build && yarn test:build",
"test:build": "cd ./build && cross-env ULX_DATA_DIR=.data-test NODE_ENV=test jest",
"test:debug": "yarn build && yarn copy:build && cd ./build && cross-env ULX_DATA_DIR=.data-test NODE_ENV=test node --inspect node_modules/.bin/jest --runInBand",
"test:debug": "yarn build && cd ./build && cross-env ULX_DATA_DIR=.data-test NODE_ENV=test node --inspect node_modules/.bin/jest --runInBand",
"test:debug:fast": "yarn tsc && cd ./build && cross-env ULX_DATA_DIR=.data-test NODE_ENV=test node --inspect node_modules/.bin/jest --runInBand",
"lint": "eslint --cache ./",
"version:check": "ulx-repo-version-check fix",
"version:bump": "ulx-repo-version-bump"
Expand Down
10 changes: 10 additions & 0 deletions plugins/default-browser-emulator/injected-scripts/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ module.exports = {
files: ['**/*.ts'],
rules: {
'no-restricted-globals': 'off',
'no-restricted-properties': [
'error',
...Object.getOwnPropertyNames(Object).map(key => {
return { object: 'Object', property: key };
}),
...Object.getOwnPropertyNames(Reflect).map(key => {
return { object: 'Reflect', property: key };
}),
],
'no-proto': 'off',
'no-extend-native': 'off',
'no-inner-declarations': 'off',
Expand All @@ -21,6 +30,7 @@ module.exports = {
'prefer-rest-params': 'off',
'func-names': 'off',
'no-console': 'off',
'lines-around-directive': 'off',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
const triggerName = args.callbackName;
export type Args = {
callbackName: string;
};
const typedArgs = args as Args;
const triggerName = typedArgs.callbackName;

if (!self[triggerName]) throw new Error('No cookie trigger');
const cookieTrigger = ((self[triggerName] as unknown) as Function).bind(self);
const cookieTrigger = (self[triggerName] as unknown as Function).bind(self);

delete self[triggerName];

proxySetter(Document.prototype, 'cookie', (target, thisArg, cookie) => {
cookieTrigger(JSON.stringify({ cookie, origin: self.location.origin }));
return ProxyOverride.callOriginal;
replaceSetter(Document.prototype, 'cookie', (target, thisArg, argArray) => {
const cookie = argArray.at(0);
if (cookie) {
cookieTrigger(JSON.stringify({ cookie, origin: self.location.origin }));
}
return ReflectCached.apply(target, thisArg, argArray!);
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
proxyFunction(JSON, 'stringify', (target, thisArg, argArray) => {
argArray[1] = null;
argArray[2] = 2;
export type Args = never;

const result = target.apply(thisArg, argArray);
replaceFunction(JSON, 'stringify', (target, thisArg, argArray) => {
const result = ReflectCached.apply(target, thisArg, [argArray.at(0), null, 2]);
console.log(result);

return result;
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
export type Args = {
deviceId?: string;
groupId?: string
};
const typedArgs = args as Args;

if (
navigator.mediaDevices &&
navigator.mediaDevices.enumerateDevices &&
navigator.mediaDevices.enumerateDevices.name !== 'bound reportBlock'
) {
const videoDevice = {
deviceId: args.deviceId,
groupId: args.groupId,
deviceId: typedArgs.deviceId,
groupId: typedArgs.groupId,
kind: 'videoinput',
label: '',
};
proxyFunction(MediaDevices.prototype, 'enumerateDevices', (func, thisObj, ...args) => {
return func.apply(thisObj, args).then(list => {
replaceFunction(MediaDevices.prototype, 'enumerateDevices', (target, thisArg, argArray) => {
return (ReflectCached.apply(target, thisArg, argArray) as Promise<any>).then(list => {
if (list.find(x => x.kind === 'videoinput')) return list;
list.push(videoDevice);
return list;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// @ts-ignore
const { audioCodecs, videoCodecs } = args;
export type Args = {
audioCodecs: any;
videoCodecs: any;
};
const typedArgs = args as Args;

const { audioCodecs, videoCodecs } = typedArgs;

if ('RTCRtpSender' in self && RTCRtpSender.prototype) {
proxyFunction(RTCRtpSender, 'getCapabilities', function (target, thisArg, argArray) {
replaceFunction(RTCRtpSender, 'getCapabilities', function (target, thisArg, argArray) {
const kind = argArray && argArray.length ? argArray[0] : null;
const args = kind ? [kind] : undefined;
const capabilities = target.apply(thisArg, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
export type Args = never;

if (typeof SharedWorker === 'undefined') {
// @ts-ignore
return;
}

const OriginalSharedWorker = SharedWorker;
const originalSharedWorkerProperties = ObjectCached.getOwnPropertyDescriptors(SharedWorker);

// shared workers created from blobs don't automatically pause in devtools, so we have to manipulate
proxyConstructor(self, 'SharedWorker', (target, argArray) => {
if (!argArray?.length) return ReflectCached.construct(target, argArray);
ObjectCached.defineProperty(self, 'SharedWorker', {
// eslint-disable-next-line object-shorthand
value: function SharedWorker(this, scriptURL, options) {
// eslint-disable-next-line strict
'use strict';
if (!new.target) {
return ReflectCached.apply(OriginalSharedWorker, this, [scriptURL, options]);
}

const [url] = argArray;
if (!url?.toString().startsWith('blob:')) {
return ReflectCached.construct(target, argArray);
}
let isBlob = false;
try {
isBlob = scriptURL?.toString().startsWith('blob:');
} catch {}
if (!isBlob) {
return ReflectCached.construct(OriginalSharedWorker, [scriptURL, options], new.target);
}

// read blob contents synchronously
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.send();
const text = xhr.response;
// read blob contents synchronously
const xhr = new XMLHttpRequest();
xhr.open('GET', scriptURL, false);
xhr.send();
const text = xhr.response;

const script = createScript(text);

const newBlob = new Blob([script]);
return ReflectCached.construct(OriginalSharedWorker, [URL.createObjectURL(newBlob), options], new.target);
},
});

ObjectCached.defineProperties(SharedWorker, originalSharedWorkerProperties);
SharedWorker.prototype.constructor = SharedWorker;
toOriginalFn.set(SharedWorker, OriginalSharedWorker);

function createScript(originalScript: string) {
const script = `
function original() {
${text};
${originalScript};
}
(async function runWhenReady() {
Expand All @@ -34,37 +60,33 @@ proxyConstructor(self, 'SharedWorker', (target, argArray) => {
setTimeout(()=> {
removeEventListener('connect', storeEvent);
original();
events.forEach(ev => onconnect(ev));
events.forEach(ev => dispatchEvent(ev));
delete events;
}, 0);
}
function isInjectedDone() {
// See proxyUtils
function getSharedStorage() {
try {
// We can use this to check if injected logic is loaded
return Error['${sourceUrl}'];
return Function.prototype.toString('${sourceUrl}');
} catch {
return false;
return undefined;
}
}
if (isInjectedDone()) {
if (getSharedStorage()?.ready) {
originalAsSync();
return;
}
// Keep checking until we are ready
const interval = setInterval(() => {
if (!isInjectedDone()) {
return
if (getSharedStorage()?.ready) {
clearInterval(interval);
originalAsSync();
}
clearInterval(interval);
originalAsSync();
}, 20);
})()
`;

const newBlob = new Blob([script]);
return ReflectCached.construct(target, [URL.createObjectURL(newBlob)]);
});
return script;
}
Loading

0 comments on commit 0162fe3

Please sign in to comment.