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

Commit

Permalink
feat(plugins): ability to configure plugins
Browse files Browse the repository at this point in the history
Add an ability to provide configuration per plugin
  • Loading branch information
soundofspace authored Jun 26, 2024
1 parent f53f888 commit 5bc079b
Show file tree
Hide file tree
Showing 17 changed files with 319 additions and 92 deletions.
5 changes: 3 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 } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import { IUnblockedPluginClass, UnblockedPluginConfig } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import { nanoid } from 'nanoid';
import env from '../env';
import ICommandMarker from '../interfaces/ICommandMarker';
Expand All @@ -26,6 +26,7 @@ const { log } = Log(module);
export interface IAgentCreateOptions extends Omit<IEmulationProfile, keyof IEmulationOptions> {
id?: string;
plugins?: IUnblockedPluginClass[];
pluginConfigs?: UnblockedPluginConfig;
commandMarker?: ICommandMarker;
}

Expand Down Expand Up @@ -83,7 +84,7 @@ export default class Agent extends TypedEventEmitter<{ close: void }> {
sessionId: this.id,
});

this.plugins = new Plugins(options, options.plugins);
this.plugins = new Plugins(options, options.plugins, options.pluginConfigs);
this.mitmRequestSession = new RequestSession(
this.id,
this.plugins,
Expand Down
29 changes: 21 additions & 8 deletions agent/main/lib/Plugins.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile';
import IUnblockedPlugins from '@ulixee/unblocked-specification/plugin/IUnblockedPlugins';
import IUnblockedPlugin, {
IUnblockedPluginClass,
IUnblockedPluginClass, PluginConfigs,
} from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import { URL } from 'url';
import {
Expand Down Expand Up @@ -69,22 +69,35 @@ export default class Plugins implements IUnblockedPlugins {
websiteHasFirstPartyInteraction: [],
};

constructor(emulationProfile: IEmulationProfile, pluginClasses: IUnblockedPluginClass[]) {
constructor(
emulationProfile: IEmulationProfile,
pluginClasses: IUnblockedPluginClass[],
pluginConfigs: PluginConfigs = {},
) {
this.profile = emulationProfile ?? {};
this.profile.options ??= {};
Object.assign(this.profile, this.profile.options);
pluginClasses ??= [];
pluginConfigs ??= {};

if (this.profile.browserEngine instanceof ChromeApp) {
this.profile.browserEngine = new ChromeEngine(this.profile.browserEngine);
}

if (pluginClasses?.length) {
const PluginClasses = pluginClasses.filter(x => x.shouldActivate?.(this.profile) ?? true);
for (const Plugin of PluginClasses) {
const plugin = new Plugin(this.profile);
this.instances.push(plugin);
this.hook(plugin, false);
for (const Plugin of pluginClasses) {
const config = pluginConfigs[Plugin.id];
let plugin: IUnblockedPlugin<any>;
// true shortcircuits and doesn't check shouldActivate
if (config === true) {
plugin = new Plugin(this.profile);
} else if (config === false || Plugin.shouldActivate?.(this.profile, config) === false) {
continue;
} else {
plugin = new Plugin(this.profile, config);
}

this.instances.push(plugin);
this.hook(plugin, false);
}

if (!this.profile.browserEngine && !pluginClasses?.length) {
Expand Down
8 changes: 7 additions & 1 deletion agent/main/lib/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import EventSubscriber from '@ulixee/commons/lib/EventSubscriber';
import IResolvablePromise from '@ulixee/commons/interfaces/IResolvablePromise';
import IBrowserUserConfig from '@ulixee/unblocked-specification/agent/browser/IBrowserUserConfig';
import { IHooksProvider } from '@ulixee/unblocked-specification/agent/hooks/IHooks';
import { IUnblockedPluginClass } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import {
IUnblockedPluginClass,
UnblockedPluginConfig,
} from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile';
import IRegisteredEventListener from '@ulixee/commons/interfaces/IRegisteredEventListener';
import Browser from './Browser';
Expand All @@ -28,6 +31,7 @@ interface ICreatePoolOptions {
certificateStore?: ICertificateStore;
defaultBrowserEngine?: IBrowserEngine;
plugins?: IUnblockedPluginClass[];
pluginConfigs?: UnblockedPluginConfig;
dataDir?: string;
logger?: IBoundLog;
}
Expand All @@ -52,6 +56,7 @@ export default class Pool extends TypedEventEmitter<{
public readonly agentsById = new Map<string, Agent>();
public sharedMitmProxy: MitmProxy;
public plugins: IUnblockedPluginClass[] = [];
public pluginConfigs: UnblockedPluginConfig = {};

#activeAgentsCount = 0;
#waitingForAvailability: {
Expand Down Expand Up @@ -94,6 +99,7 @@ export default class Pool extends TypedEventEmitter<{
};
}
options.plugins ??= [...this.plugins];
options.pluginConfigs ??= structuredClone(this.pluginConfigs);
const agent = new Agent(options, this);
this.agentsById.set(agent.id, agent);
this.emit('agent-created', { agent });
Expand Down
100 changes: 95 additions & 5 deletions agent/main/test/Plugins.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { UnblockedPluginClassDecorator } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import IUnblockedPlugin, {
UnblockedPluginClassDecorator,
} from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin';
import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile';
import Plugins from '../lib/Plugins';

Expand All @@ -7,6 +9,7 @@ test('each plugin should be given a chance to pre-configure the profile', () =>

@UnblockedPluginClassDecorator
class Plugins1 {
static id = 'Plugins1';
static shouldActivate(profile: IEmulationProfile): boolean {
plugin1Activate();
profile.timezoneId = 'tz1';
Expand All @@ -17,6 +20,7 @@ test('each plugin should be given a chance to pre-configure the profile', () =>

@UnblockedPluginClassDecorator
class Plugins2 {
static id = 'Plugins2';
static shouldActivate(profile: IEmulationProfile): boolean {
plugin2Activate();
expect(profile.timezoneId).toBe('tz1');
Expand All @@ -29,6 +33,7 @@ test('each plugin should be given a chance to pre-configure the profile', () =>
// It should not include Pluginss that choose not to participate
@UnblockedPluginClassDecorator
class Plugins3 {
static id = 'Plugins3';
static shouldActivate(profile: IEmulationProfile): boolean {
plugin3Activate();
if (profile.timezoneId === 'tz2') return false;
Expand All @@ -37,9 +42,11 @@ test('each plugin should be given a chance to pre-configure the profile', () =>

// It should include Pluginss that don't implement shouldActivate
@UnblockedPluginClassDecorator
class Plugins4 {}
class Plugins4 {
static id = 'Plugins4';
}

const plugins = new Plugins({}, [Plugins1, Plugins2, Plugins3, Plugins4]);
const plugins = new Plugins({}, [Plugins1, Plugins2, Plugins3, Plugins4], {});

expect(plugin1Activate).toHaveBeenCalled();
expect(plugin2Activate).toHaveBeenCalled();
Expand All @@ -53,6 +60,7 @@ test('should only allow take the last implementation of playInteractions', async

@UnblockedPluginClassDecorator
class Plugins1 {
static id = 'Plugins1';
playInteractions(): Promise<void> {
play1Fn();
return Promise.resolve();
Expand All @@ -62,13 +70,14 @@ test('should only allow take the last implementation of playInteractions', async
const play2Fn = jest.fn();
@UnblockedPluginClassDecorator
class Plugins2 {
static id = 'Plugins2';
playInteractions(): Promise<void> {
play2Fn();
return Promise.resolve();
}
}

const plugins = new Plugins({}, [Plugins1, Plugins2]);
const plugins = new Plugins({}, [Plugins1, Plugins2], {});
await plugins.playInteractions([], jest.fn(), null);
expect(play1Fn).not.toHaveBeenCalled();
expect(play2Fn).toHaveBeenCalledTimes(1);
Expand All @@ -80,6 +89,7 @@ test("plugin implementations should be called in the order they're installed", a
const callOrder = [];
@UnblockedPluginClassDecorator
class Plugins1 {
static id = 'Plugins1';
onNewPage(): Promise<void> {
newPage1Fn();
callOrder.push(newPage1Fn);
Expand All @@ -89,16 +99,96 @@ test("plugin implementations should be called in the order they're installed", a
const newPage2Fn = jest.fn();
@UnblockedPluginClassDecorator
class Plugins2 {
static id = 'Plugins2';
onNewPage(): Promise<void> {
newPage2Fn();
callOrder.push(newPage2Fn);
return Promise.resolve();
}
}

const plugins = new Plugins({}, [Plugins1, Plugins2]);
const plugins = new Plugins({}, [Plugins1, Plugins2], {});
await plugins.onNewPage({} as any);
expect(newPage1Fn).toHaveBeenCalledTimes(1);
expect(newPage2Fn).toHaveBeenCalledTimes(1);
expect(callOrder).toEqual([newPage1Fn, newPage2Fn]);
});

test('should pass config to correct plugin', async () => {
const plugin1Config = { plugin1: true };
const plugin2Config = { plugin2: true };

@UnblockedPluginClassDecorator
class Plugins1 {
static id = 'Plugins1';

constructor(_emulationProfile: IEmulationProfile, config?: typeof plugin1Config) {
expect(config).toEqual(plugin1Config);
}
}

@UnblockedPluginClassDecorator
class Plugins2 implements IUnblockedPlugin {
static id = 'Plugins2';

constructor(emulationProfile: IEmulationProfile, config?: typeof plugin1Config) {
expect(config).toEqual(plugin2Config);
}
}

const _plugins = new Plugins({}, [Plugins1, Plugins2], {
[Plugins1.id]: plugin1Config,
[Plugins2.id]: plugin2Config,
});
});

test('should disable plugin if config = false', async () => { @UnblockedPluginClassDecorator
class Plugins1 {
static id = 'Plugins1';
}

class Plugins2 {
static id = 'Plugins2';
}

const plugins = new Plugins({}, [Plugins1, Plugins2], {
[Plugins1.id]: true,
[Plugins2.id]: false,
});

expect(plugins.instances).toHaveLength(1);
});

test('should skip shouldEnabled if config = true', async () => {
const shouldActivate1 = jest.fn();
const shouldActivate2 = jest.fn();

@UnblockedPluginClassDecorator
class Plugins1 {
static id = 'Plugins1';

static shouldActivate(): boolean {
shouldActivate1();
return false;
}
}

@UnblockedPluginClassDecorator
class Plugins2 {
static id = 'Plugins2';

static shouldActivate(): boolean {
shouldActivate2();
return false;
}
}

const plugins = new Plugins({}, [Plugins1, Plugins2], {
[Plugins1.id]: true,
[Plugins2.id]: undefined,
});

expect(shouldActivate1).not.toHaveBeenCalled();
expect(shouldActivate2).toHaveBeenCalled();
expect(plugins.instances).toHaveLength(1);
});
1 change: 1 addition & 0 deletions agent/main/test/Pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ describe('Pool tests', () => {

@UnblockedPluginClassDecorator
class TestPlugin {
static id = 'TestPlugin';
static shouldActivate(profile: IEmulationProfile): boolean {
profile.upstreamProxyUrl = upstreamProxyUrl;
profile.options.disableMitm = true;
Expand Down
20 changes: 18 additions & 2 deletions plugins/default-browser-emulator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import IUserAgentData from './interfaces/IUserAgentData';
import UserAgentOptions from './lib/UserAgentOptions';
import BrowserEngineOptions from './lib/BrowserEngineOptions';

import { name } from './package.json';
import IBrowserEmulatorConfig, { InjectedScript } from './interfaces/IBrowserEmulatorConfig';

// Configuration to rotate out the default browser id. Used for testing different browsers via cli
const defaultBrowserId = process.env.ULX_DEFAULT_BROWSER_ID;

Expand All @@ -63,12 +66,23 @@ export interface IEmulatorOptions {

let hasWarnedAboutProxyIp = false;

const allEnabled = Object.values(InjectedScript).reduce((acc, value) => {
return { ...acc, [value]: true };
}, {} as IBrowserEmulatorConfig);

export const defaultConfig = {
...allEnabled,
[InjectedScript.JSON_STRINGIFY]: false,
};

@UnblockedPluginClassDecorator
export default class DefaultBrowserEmulator<T = IEmulatorOptions> implements IUnblockedPlugin<T> {
public static id = name;
// Should the system attempt to manipulate tcp settings to match the emulated OS. NOTE that this can affect tcp performance.
public static enableTcpEmulation = false;
public readonly logger: IBoundLog;
public readonly emulationProfile: IEmulationProfile<T>;
public readonly config: IBrowserEmulatorConfig;

public get userAgentString(): string {
return this.emulationProfile.userAgentOption.string;
Expand All @@ -84,6 +98,7 @@ export default class DefaultBrowserEmulator<T = IEmulatorOptions> implements IUn

protected get domOverridesBuilder(): DomOverridesBuilder {
this.#domOverridesBuilder ??= loadDomOverrides(
this.config,
this.emulationProfile,
this.data,
this.userAgentData,
Expand All @@ -96,7 +111,8 @@ export default class DefaultBrowserEmulator<T = IEmulatorOptions> implements IUn

#domOverridesBuilder: DomOverridesBuilder;

constructor(emulationProfile: IEmulationProfile<T>) {
constructor(emulationProfile: IEmulationProfile<T>, config?: IBrowserEmulatorConfig) {
this.config = config ?? defaultConfig;
this.logger = emulationProfile.logger ?? log.createChild(module);
this.emulationProfile = emulationProfile;
this.data = dataLoader.as(emulationProfile.userAgentOption) as any;
Expand Down Expand Up @@ -169,7 +185,7 @@ export default class DefaultBrowserEmulator<T = IEmulatorOptions> implements IUn
this.logger.info('PublicIp Lookup', {
...upstreamProxyIpMask,
});
this.domOverridesBuilder.add('webrtc', {
this.domOverridesBuilder.add(InjectedScript.WEBRTC, {
localIp: upstreamProxyIpMask.publicIp,
proxyIp: upstreamProxyIpMask.proxyIp,
});
Expand Down
3 changes: 3 additions & 0 deletions plugins/default-browser-emulator/injected-scripts/console.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const mode = args.mode;

ObjectCached.keys(console).forEach(key => {
proxyFunction(console, key, (target, thisArg, args) => {
if (mode === 'disableConsole') return undefined;
args = replaceErrorStackWithOriginal(args);
return ReflectCached.apply(target, thisArg, args);
});
Expand Down
Loading

0 comments on commit 5bc079b

Please sign in to comment.