tor-browser

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

Browser.ts (8150B)


      1 /**
      2 * @license
      3 * Copyright 2022 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {ChildProcess} from 'node:child_process';
      8 
      9 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
     10 
     11 import type {BrowserEvents} from '../api/Browser.js';
     12 import {
     13  Browser,
     14  BrowserEvent,
     15  type BrowserCloseCallback,
     16  type BrowserContextOptions,
     17  type DebugInfo,
     18 } from '../api/Browser.js';
     19 import {BrowserContextEvent} from '../api/BrowserContext.js';
     20 import type {Page} from '../api/Page.js';
     21 import type {Target} from '../api/Target.js';
     22 import type {Connection as CdpConnection} from '../cdp/Connection.js';
     23 import type {SupportedWebDriverCapabilities} from '../common/ConnectOptions.js';
     24 import {EventEmitter} from '../common/EventEmitter.js';
     25 import {debugError} from '../common/util.js';
     26 import type {Viewport} from '../common/Viewport.js';
     27 import {bubble} from '../util/decorators.js';
     28 
     29 import {BidiBrowserContext} from './BrowserContext.js';
     30 import type {BidiConnection} from './Connection.js';
     31 import type {Browser as BrowserCore} from './core/Browser.js';
     32 import {Session} from './core/Session.js';
     33 import type {UserContext} from './core/UserContext.js';
     34 import {BidiBrowserTarget} from './Target.js';
     35 
     36 /**
     37 * @internal
     38 */
     39 export interface BidiBrowserOptions {
     40  process?: ChildProcess;
     41  closeCallback?: BrowserCloseCallback;
     42  connection: BidiConnection;
     43  cdpConnection?: CdpConnection;
     44  defaultViewport: Viewport | null;
     45  acceptInsecureCerts?: boolean;
     46  capabilities?: SupportedWebDriverCapabilities;
     47 }
     48 
     49 /**
     50 * @internal
     51 */
     52 export class BidiBrowser extends Browser {
     53  readonly protocol = 'webDriverBiDi';
     54 
     55  static readonly subscribeModules: [string, ...string[]] = [
     56    'browsingContext',
     57    'network',
     58    'log',
     59    'script',
     60    'input',
     61  ];
     62  static readonly subscribeCdpEvents: Bidi.Cdp.EventNames[] = [
     63    // Coverage
     64    'goog:cdp.Debugger.scriptParsed',
     65    'goog:cdp.CSS.styleSheetAdded',
     66    'goog:cdp.Runtime.executionContextsCleared',
     67    // Tracing
     68    'goog:cdp.Tracing.tracingComplete',
     69    // TODO: subscribe to all CDP events in the future.
     70    'goog:cdp.Network.requestWillBeSent',
     71    'goog:cdp.Debugger.scriptParsed',
     72    'goog:cdp.Page.screencastFrame',
     73  ];
     74 
     75  static async create(opts: BidiBrowserOptions): Promise<BidiBrowser> {
     76    const session = await Session.from(opts.connection, {
     77      firstMatch: opts.capabilities?.firstMatch,
     78      alwaysMatch: {
     79        ...opts.capabilities?.alwaysMatch,
     80        // Capabilities that come from Puppeteer's API take precedence.
     81        acceptInsecureCerts: opts.acceptInsecureCerts,
     82        unhandledPromptBehavior: {
     83          default: Bidi.Session.UserPromptHandlerType.Ignore,
     84        },
     85        webSocketUrl: true,
     86        // Puppeteer with WebDriver BiDi does not support prerendering
     87        // yet because WebDriver BiDi behavior is not specified. See
     88        // https://github.com/w3c/webdriver-bidi/issues/321.
     89        'goog:prerenderingDisabled': true,
     90      },
     91    });
     92 
     93    await session.subscribe(
     94      session.capabilities.browserName.toLocaleLowerCase().includes('firefox')
     95        ? BidiBrowser.subscribeModules
     96        : [...BidiBrowser.subscribeModules, ...BidiBrowser.subscribeCdpEvents],
     97    );
     98 
     99    const browser = new BidiBrowser(session.browser, opts);
    100    browser.#initialize();
    101    return browser;
    102  }
    103 
    104  @bubble()
    105  accessor #trustedEmitter = new EventEmitter<BrowserEvents>();
    106 
    107  #process?: ChildProcess;
    108  #closeCallback?: BrowserCloseCallback;
    109  #browserCore: BrowserCore;
    110  #defaultViewport: Viewport | null;
    111  #browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
    112  #target = new BidiBrowserTarget(this);
    113  #cdpConnection?: CdpConnection;
    114 
    115  private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
    116    super();
    117    this.#process = opts.process;
    118    this.#closeCallback = opts.closeCallback;
    119    this.#browserCore = browserCore;
    120    this.#defaultViewport = opts.defaultViewport;
    121    this.#cdpConnection = opts.cdpConnection;
    122  }
    123 
    124  #initialize() {
    125    // Initializing existing contexts.
    126    for (const userContext of this.#browserCore.userContexts) {
    127      this.#createBrowserContext(userContext);
    128    }
    129 
    130    this.#browserCore.once('disconnected', () => {
    131      this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined);
    132      this.#trustedEmitter.removeAllListeners();
    133    });
    134    this.#process?.once('close', () => {
    135      this.#browserCore.dispose('Browser process exited.', true);
    136      this.connection.dispose();
    137    });
    138  }
    139 
    140  get #browserName() {
    141    return this.#browserCore.session.capabilities.browserName;
    142  }
    143  get #browserVersion() {
    144    return this.#browserCore.session.capabilities.browserVersion;
    145  }
    146 
    147  get cdpSupported(): boolean {
    148    return this.#cdpConnection !== undefined;
    149  }
    150 
    151  get cdpConnection(): CdpConnection | undefined {
    152    return this.#cdpConnection;
    153  }
    154 
    155  override async userAgent(): Promise<string> {
    156    return this.#browserCore.session.capabilities.userAgent;
    157  }
    158 
    159  #createBrowserContext(userContext: UserContext) {
    160    const browserContext = BidiBrowserContext.from(this, userContext, {
    161      defaultViewport: this.#defaultViewport,
    162    });
    163    this.#browserContexts.set(userContext, browserContext);
    164 
    165    browserContext.trustedEmitter.on(
    166      BrowserContextEvent.TargetCreated,
    167      target => {
    168        this.#trustedEmitter.emit(BrowserEvent.TargetCreated, target);
    169      },
    170    );
    171    browserContext.trustedEmitter.on(
    172      BrowserContextEvent.TargetChanged,
    173      target => {
    174        this.#trustedEmitter.emit(BrowserEvent.TargetChanged, target);
    175      },
    176    );
    177    browserContext.trustedEmitter.on(
    178      BrowserContextEvent.TargetDestroyed,
    179      target => {
    180        this.#trustedEmitter.emit(BrowserEvent.TargetDestroyed, target);
    181      },
    182    );
    183 
    184    return browserContext;
    185  }
    186 
    187  get connection(): BidiConnection {
    188    // SAFETY: We only have one implementation.
    189    return this.#browserCore.session.connection as BidiConnection;
    190  }
    191 
    192  override wsEndpoint(): string {
    193    return this.connection.url;
    194  }
    195 
    196  override async close(): Promise<void> {
    197    if (this.connection.closed) {
    198      return;
    199    }
    200 
    201    try {
    202      await this.#browserCore.close();
    203      await this.#closeCallback?.call(null);
    204    } catch (error) {
    205      // Fail silently.
    206      debugError(error);
    207    } finally {
    208      this.connection.dispose();
    209    }
    210  }
    211 
    212  override get connected(): boolean {
    213    return !this.#browserCore.disconnected;
    214  }
    215 
    216  override process(): ChildProcess | null {
    217    return this.#process ?? null;
    218  }
    219 
    220  override async createBrowserContext(
    221    _options?: BrowserContextOptions,
    222  ): Promise<BidiBrowserContext> {
    223    const userContext = await this.#browserCore.createUserContext();
    224    return this.#createBrowserContext(userContext);
    225  }
    226 
    227  override async version(): Promise<string> {
    228    return `${this.#browserName}/${this.#browserVersion}`;
    229  }
    230 
    231  override browserContexts(): BidiBrowserContext[] {
    232    return [...this.#browserCore.userContexts].map(context => {
    233      return this.#browserContexts.get(context)!;
    234    });
    235  }
    236 
    237  override defaultBrowserContext(): BidiBrowserContext {
    238    return this.#browserContexts.get(this.#browserCore.defaultUserContext)!;
    239  }
    240 
    241  override newPage(): Promise<Page> {
    242    return this.defaultBrowserContext().newPage();
    243  }
    244 
    245  override installExtension(path: string): Promise<string> {
    246    return this.#browserCore.installExtension(path);
    247  }
    248 
    249  override async uninstallExtension(id: string): Promise<void> {
    250    await this.#browserCore.uninstallExtension(id);
    251  }
    252 
    253  override targets(): Target[] {
    254    return [
    255      this.#target,
    256      ...this.browserContexts().flatMap(context => {
    257        return context.targets();
    258      }),
    259    ];
    260  }
    261 
    262  override target(): BidiBrowserTarget {
    263    return this.#target;
    264  }
    265 
    266  override async disconnect(): Promise<void> {
    267    try {
    268      await this.#browserCore.session.end();
    269    } catch (error) {
    270      // Fail silently.
    271      debugError(error);
    272    } finally {
    273      this.connection.dispose();
    274    }
    275  }
    276 
    277  override get debugInfo(): DebugInfo {
    278    return {
    279      pendingProtocolErrors: this.connection.getPendingProtocolErrors(),
    280    };
    281  }
    282 }