tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Browser.ts (12103B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {ChildProcess} from 'node:child_process';
      8 
      9 import type {Protocol} from 'devtools-protocol';
     10 
     11 import type {DebugInfo} from '../api/Browser.js';
     12 import {
     13  Browser as BrowserBase,
     14  BrowserEvent,
     15  type BrowserCloseCallback,
     16  type BrowserContextOptions,
     17  type IsPageTargetCallback,
     18  type TargetFilterCallback,
     19 } from '../api/Browser.js';
     20 import {BrowserContextEvent} from '../api/BrowserContext.js';
     21 import {CDPSessionEvent} from '../api/CDPSession.js';
     22 import type {Page} from '../api/Page.js';
     23 import type {Target} from '../api/Target.js';
     24 import type {DownloadBehavior} from '../common/DownloadBehavior.js';
     25 import type {Viewport} from '../common/Viewport.js';
     26 
     27 import {CdpBrowserContext} from './BrowserContext.js';
     28 import type {CdpCDPSession} from './CdpSession.js';
     29 import type {Connection} from './Connection.js';
     30 import {
     31  DevToolsTarget,
     32  InitializationStatus,
     33  OtherTarget,
     34  PageTarget,
     35  WorkerTarget,
     36  type CdpTarget,
     37 } from './Target.js';
     38 import {TargetManagerEvent} from './TargetManageEvents.js';
     39 import {TargetManager} from './TargetManager.js';
     40 
     41 /**
     42 * @internal
     43 */
     44 export class CdpBrowser extends BrowserBase {
     45  readonly protocol = 'cdp';
     46 
     47  static async _create(
     48    connection: Connection,
     49    contextIds: string[],
     50    acceptInsecureCerts: boolean,
     51    defaultViewport?: Viewport | null,
     52    downloadBehavior?: DownloadBehavior,
     53    process?: ChildProcess,
     54    closeCallback?: BrowserCloseCallback,
     55    targetFilterCallback?: TargetFilterCallback,
     56    isPageTargetCallback?: IsPageTargetCallback,
     57    waitForInitiallyDiscoveredTargets = true,
     58  ): Promise<CdpBrowser> {
     59    const browser = new CdpBrowser(
     60      connection,
     61      contextIds,
     62      defaultViewport,
     63      process,
     64      closeCallback,
     65      targetFilterCallback,
     66      isPageTargetCallback,
     67      waitForInitiallyDiscoveredTargets,
     68    );
     69    if (acceptInsecureCerts) {
     70      await connection.send('Security.setIgnoreCertificateErrors', {
     71        ignore: true,
     72      });
     73    }
     74    await browser._attach(downloadBehavior);
     75    return browser;
     76  }
     77  #defaultViewport?: Viewport | null;
     78  #process?: ChildProcess;
     79  #connection: Connection;
     80  #closeCallback: BrowserCloseCallback;
     81  #targetFilterCallback: TargetFilterCallback;
     82  #isPageTargetCallback!: IsPageTargetCallback;
     83  #defaultContext: CdpBrowserContext;
     84  #contexts = new Map<string, CdpBrowserContext>();
     85  #targetManager: TargetManager;
     86 
     87  constructor(
     88    connection: Connection,
     89    contextIds: string[],
     90    defaultViewport?: Viewport | null,
     91    process?: ChildProcess,
     92    closeCallback?: BrowserCloseCallback,
     93    targetFilterCallback?: TargetFilterCallback,
     94    isPageTargetCallback?: IsPageTargetCallback,
     95    waitForInitiallyDiscoveredTargets = true,
     96  ) {
     97    super();
     98    this.#defaultViewport = defaultViewport;
     99    this.#process = process;
    100    this.#connection = connection;
    101    this.#closeCallback = closeCallback || (() => {});
    102    this.#targetFilterCallback =
    103      targetFilterCallback ||
    104      (() => {
    105        return true;
    106      });
    107    this.#setIsPageTargetCallback(isPageTargetCallback);
    108    this.#targetManager = new TargetManager(
    109      connection,
    110      this.#createTarget,
    111      this.#targetFilterCallback,
    112      waitForInitiallyDiscoveredTargets,
    113    );
    114    this.#defaultContext = new CdpBrowserContext(this.#connection, this);
    115    for (const contextId of contextIds) {
    116      this.#contexts.set(
    117        contextId,
    118        new CdpBrowserContext(this.#connection, this, contextId),
    119      );
    120    }
    121  }
    122 
    123  #emitDisconnected = () => {
    124    this.emit(BrowserEvent.Disconnected, undefined);
    125  };
    126 
    127  async _attach(downloadBehavior: DownloadBehavior | undefined): Promise<void> {
    128    this.#connection.on(CDPSessionEvent.Disconnected, this.#emitDisconnected);
    129    if (downloadBehavior) {
    130      await this.#defaultContext.setDownloadBehavior(downloadBehavior);
    131    }
    132    this.#targetManager.on(
    133      TargetManagerEvent.TargetAvailable,
    134      this.#onAttachedToTarget,
    135    );
    136    this.#targetManager.on(
    137      TargetManagerEvent.TargetGone,
    138      this.#onDetachedFromTarget,
    139    );
    140    this.#targetManager.on(
    141      TargetManagerEvent.TargetChanged,
    142      this.#onTargetChanged,
    143    );
    144    this.#targetManager.on(
    145      TargetManagerEvent.TargetDiscovered,
    146      this.#onTargetDiscovered,
    147    );
    148    await this.#targetManager.initialize();
    149  }
    150 
    151  _detach(): void {
    152    this.#connection.off(CDPSessionEvent.Disconnected, this.#emitDisconnected);
    153    this.#targetManager.off(
    154      TargetManagerEvent.TargetAvailable,
    155      this.#onAttachedToTarget,
    156    );
    157    this.#targetManager.off(
    158      TargetManagerEvent.TargetGone,
    159      this.#onDetachedFromTarget,
    160    );
    161    this.#targetManager.off(
    162      TargetManagerEvent.TargetChanged,
    163      this.#onTargetChanged,
    164    );
    165    this.#targetManager.off(
    166      TargetManagerEvent.TargetDiscovered,
    167      this.#onTargetDiscovered,
    168    );
    169  }
    170 
    171  override process(): ChildProcess | null {
    172    return this.#process ?? null;
    173  }
    174 
    175  _targetManager(): TargetManager {
    176    return this.#targetManager;
    177  }
    178 
    179  #setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
    180    this.#isPageTargetCallback =
    181      isPageTargetCallback ||
    182      ((target: Target): boolean => {
    183        return (
    184          target.type() === 'page' ||
    185          target.type() === 'background_page' ||
    186          target.type() === 'webview'
    187        );
    188      });
    189  }
    190 
    191  _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
    192    return this.#isPageTargetCallback;
    193  }
    194 
    195  override async createBrowserContext(
    196    options: BrowserContextOptions = {},
    197  ): Promise<CdpBrowserContext> {
    198    const {proxyServer, proxyBypassList, downloadBehavior} = options;
    199 
    200    const {browserContextId} = await this.#connection.send(
    201      'Target.createBrowserContext',
    202      {
    203        proxyServer,
    204        proxyBypassList: proxyBypassList && proxyBypassList.join(','),
    205      },
    206    );
    207    const context = new CdpBrowserContext(
    208      this.#connection,
    209      this,
    210      browserContextId,
    211    );
    212    if (downloadBehavior) {
    213      await context.setDownloadBehavior(downloadBehavior);
    214    }
    215    this.#contexts.set(browserContextId, context);
    216    return context;
    217  }
    218 
    219  override browserContexts(): CdpBrowserContext[] {
    220    return [this.#defaultContext, ...Array.from(this.#contexts.values())];
    221  }
    222 
    223  override defaultBrowserContext(): CdpBrowserContext {
    224    return this.#defaultContext;
    225  }
    226 
    227  async _disposeContext(contextId?: string): Promise<void> {
    228    if (!contextId) {
    229      return;
    230    }
    231    await this.#connection.send('Target.disposeBrowserContext', {
    232      browserContextId: contextId,
    233    });
    234    this.#contexts.delete(contextId);
    235  }
    236 
    237  #createTarget = (
    238    targetInfo: Protocol.Target.TargetInfo,
    239    session?: CdpCDPSession,
    240  ) => {
    241    const {browserContextId} = targetInfo;
    242    const context =
    243      browserContextId && this.#contexts.has(browserContextId)
    244        ? this.#contexts.get(browserContextId)
    245        : this.#defaultContext;
    246 
    247    if (!context) {
    248      throw new Error('Missing browser context');
    249    }
    250 
    251    const createSession = (isAutoAttachEmulated: boolean) => {
    252      return this.#connection._createSession(targetInfo, isAutoAttachEmulated);
    253    };
    254    const otherTarget = new OtherTarget(
    255      targetInfo,
    256      session,
    257      context,
    258      this.#targetManager,
    259      createSession,
    260    );
    261    if (targetInfo.url?.startsWith('devtools://')) {
    262      return new DevToolsTarget(
    263        targetInfo,
    264        session,
    265        context,
    266        this.#targetManager,
    267        createSession,
    268        this.#defaultViewport ?? null,
    269      );
    270    }
    271    if (this.#isPageTargetCallback(otherTarget)) {
    272      return new PageTarget(
    273        targetInfo,
    274        session,
    275        context,
    276        this.#targetManager,
    277        createSession,
    278        this.#defaultViewport ?? null,
    279      );
    280    }
    281    if (
    282      targetInfo.type === 'service_worker' ||
    283      targetInfo.type === 'shared_worker'
    284    ) {
    285      return new WorkerTarget(
    286        targetInfo,
    287        session,
    288        context,
    289        this.#targetManager,
    290        createSession,
    291      );
    292    }
    293    return otherTarget;
    294  };
    295 
    296  #onAttachedToTarget = async (target: CdpTarget) => {
    297    if (
    298      target._isTargetExposed() &&
    299      (await target._initializedDeferred.valueOrThrow()) ===
    300        InitializationStatus.SUCCESS
    301    ) {
    302      this.emit(BrowserEvent.TargetCreated, target);
    303      target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
    304    }
    305  };
    306 
    307  #onDetachedFromTarget = async (target: CdpTarget): Promise<void> => {
    308    target._initializedDeferred.resolve(InitializationStatus.ABORTED);
    309    target._isClosedDeferred.resolve();
    310    if (
    311      target._isTargetExposed() &&
    312      (await target._initializedDeferred.valueOrThrow()) ===
    313        InitializationStatus.SUCCESS
    314    ) {
    315      this.emit(BrowserEvent.TargetDestroyed, target);
    316      target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
    317    }
    318  };
    319 
    320  #onTargetChanged = ({target}: {target: CdpTarget}): void => {
    321    this.emit(BrowserEvent.TargetChanged, target);
    322    target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
    323  };
    324 
    325  #onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => {
    326    this.emit(BrowserEvent.TargetDiscovered, targetInfo);
    327  };
    328 
    329  override wsEndpoint(): string {
    330    return this.#connection.url();
    331  }
    332 
    333  override async newPage(): Promise<Page> {
    334    return await this.#defaultContext.newPage();
    335  }
    336 
    337  async _createPageInContext(contextId?: string): Promise<Page> {
    338    const {targetId} = await this.#connection.send('Target.createTarget', {
    339      url: 'about:blank',
    340      browserContextId: contextId || undefined,
    341    });
    342    const target = (await this.waitForTarget(t => {
    343      return (t as CdpTarget)._targetId === targetId;
    344    })) as CdpTarget;
    345    if (!target) {
    346      throw new Error(`Missing target for page (id = ${targetId})`);
    347    }
    348    const initialized =
    349      (await target._initializedDeferred.valueOrThrow()) ===
    350      InitializationStatus.SUCCESS;
    351    if (!initialized) {
    352      throw new Error(`Failed to create target for page (id = ${targetId})`);
    353    }
    354    const page = await target.page();
    355    if (!page) {
    356      throw new Error(
    357        `Failed to create a page for context (id = ${contextId})`,
    358      );
    359    }
    360    return page;
    361  }
    362 
    363  override async installExtension(path: string): Promise<string> {
    364    const {id} = await this.#connection.send('Extensions.loadUnpacked', {path});
    365    return id;
    366  }
    367 
    368  override uninstallExtension(id: string): Promise<void> {
    369    return this.#connection.send('Extensions.uninstall', {id});
    370  }
    371 
    372  override targets(): CdpTarget[] {
    373    return Array.from(
    374      this.#targetManager.getAvailableTargets().values(),
    375    ).filter(target => {
    376      return (
    377        target._isTargetExposed() &&
    378        target._initializedDeferred.value() === InitializationStatus.SUCCESS
    379      );
    380    });
    381  }
    382 
    383  override target(): CdpTarget {
    384    const browserTarget = this.targets().find(target => {
    385      return target.type() === 'browser';
    386    });
    387    if (!browserTarget) {
    388      throw new Error('Browser target is not found');
    389    }
    390    return browserTarget;
    391  }
    392 
    393  override async version(): Promise<string> {
    394    const version = await this.#getVersion();
    395    return version.product;
    396  }
    397 
    398  override async userAgent(): Promise<string> {
    399    const version = await this.#getVersion();
    400    return version.userAgent;
    401  }
    402 
    403  override async close(): Promise<void> {
    404    await this.#closeCallback.call(null);
    405    await this.disconnect();
    406  }
    407 
    408  override disconnect(): Promise<void> {
    409    this.#targetManager.dispose();
    410    this.#connection.dispose();
    411    this._detach();
    412    return Promise.resolve();
    413  }
    414 
    415  override get connected(): boolean {
    416    return !this.#connection._closed;
    417  }
    418 
    419  #getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
    420    return this.#connection.send('Browser.getVersion');
    421  }
    422 
    423  override get debugInfo(): DebugInfo {
    424    return {
    425      pendingProtocolErrors: this.#connection.getPendingProtocolErrors(),
    426    };
    427  }
    428 }