tor-browser

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

BrowserLauncher.ts (13548B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 import {existsSync} from 'node:fs';
      7 import {tmpdir} from 'node:os';
      8 import {join} from 'node:path';
      9 
     10 import {
     11  Browser as InstalledBrowser,
     12  CDP_WEBSOCKET_ENDPOINT_REGEX,
     13  launch,
     14  TimeoutError as BrowsersTimeoutError,
     15  WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
     16  computeExecutablePath,
     17 } from '@puppeteer/browsers';
     18 
     19 import {
     20  firstValueFrom,
     21  from,
     22  map,
     23  race,
     24  timer,
     25 } from '../../third_party/rxjs/rxjs.js';
     26 import type {Browser, BrowserCloseCallback} from '../api/Browser.js';
     27 import {CdpBrowser} from '../cdp/Browser.js';
     28 import {Connection} from '../cdp/Connection.js';
     29 import {TimeoutError} from '../common/Errors.js';
     30 import type {SupportedBrowser} from '../common/SupportedBrowser.js';
     31 import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
     32 import type {Viewport} from '../common/Viewport.js';
     33 
     34 import type {ChromeReleaseChannel, LaunchOptions} from './LaunchOptions.js';
     35 import {NodeWebSocketTransport as WebSocketTransport} from './NodeWebSocketTransport.js';
     36 import {PipeTransport} from './PipeTransport.js';
     37 import type {PuppeteerNode} from './PuppeteerNode.js';
     38 
     39 /**
     40 * @internal
     41 */
     42 export interface ResolvedLaunchArgs {
     43  isTempUserDataDir: boolean;
     44  userDataDir: string;
     45  executablePath: string;
     46  args: string[];
     47 }
     48 
     49 /**
     50 * Describes a launcher - a class that is able to create and launch a browser instance.
     51 *
     52 * @public
     53 */
     54 export abstract class BrowserLauncher {
     55  #browser: SupportedBrowser;
     56 
     57  /**
     58   * @internal
     59   */
     60  puppeteer: PuppeteerNode;
     61 
     62  /**
     63   * @internal
     64   */
     65  constructor(puppeteer: PuppeteerNode, browser: SupportedBrowser) {
     66    this.puppeteer = puppeteer;
     67    this.#browser = browser;
     68  }
     69 
     70  get browser(): SupportedBrowser {
     71    return this.#browser;
     72  }
     73 
     74  async launch(options: LaunchOptions = {}): Promise<Browser> {
     75    const {
     76      dumpio = false,
     77      enableExtensions = false,
     78      env = process.env,
     79      handleSIGINT = true,
     80      handleSIGTERM = true,
     81      handleSIGHUP = true,
     82      acceptInsecureCerts = false,
     83      defaultViewport = DEFAULT_VIEWPORT,
     84      downloadBehavior,
     85      slowMo = 0,
     86      timeout = 30000,
     87      waitForInitialPage = true,
     88      protocolTimeout,
     89    } = options;
     90 
     91    let {protocol} = options;
     92 
     93    // Default to 'webDriverBiDi' for Firefox.
     94    if (this.#browser === 'firefox' && protocol === undefined) {
     95      protocol = 'webDriverBiDi';
     96    }
     97 
     98    if (this.#browser === 'firefox' && protocol === 'cdp') {
     99      throw new Error('Connecting to Firefox using CDP is no longer supported');
    100    }
    101 
    102    const launchArgs = await this.computeLaunchArguments({
    103      ...options,
    104      protocol,
    105    });
    106 
    107    if (!existsSync(launchArgs.executablePath)) {
    108      throw new Error(
    109        `Browser was not found at the configured executablePath (${launchArgs.executablePath})`,
    110      );
    111    }
    112 
    113    const usePipe = launchArgs.args.includes('--remote-debugging-pipe');
    114 
    115    const onProcessExit = async () => {
    116      await this.cleanUserDataDir(launchArgs.userDataDir, {
    117        isTemp: launchArgs.isTempUserDataDir,
    118      });
    119    };
    120 
    121    if (
    122      this.#browser === 'firefox' &&
    123      protocol === 'webDriverBiDi' &&
    124      usePipe
    125    ) {
    126      throw new Error(
    127        'Pipe connections are not supported with Firefox and WebDriver BiDi',
    128      );
    129    }
    130 
    131    const browserProcess = launch({
    132      executablePath: launchArgs.executablePath,
    133      args: launchArgs.args,
    134      handleSIGHUP,
    135      handleSIGTERM,
    136      handleSIGINT,
    137      dumpio,
    138      env,
    139      pipe: usePipe,
    140      onExit: onProcessExit,
    141    });
    142 
    143    let browser: Browser;
    144    let cdpConnection: Connection;
    145    let closing = false;
    146 
    147    const browserCloseCallback: BrowserCloseCallback = async () => {
    148      if (closing) {
    149        return;
    150      }
    151      closing = true;
    152      await this.closeBrowser(browserProcess, cdpConnection);
    153    };
    154 
    155    try {
    156      if (this.#browser === 'firefox' && protocol === 'webDriverBiDi') {
    157        browser = await this.createBiDiBrowser(
    158          browserProcess,
    159          browserCloseCallback,
    160          {
    161            timeout,
    162            protocolTimeout,
    163            slowMo,
    164            defaultViewport,
    165            acceptInsecureCerts,
    166          },
    167        );
    168      } else {
    169        if (usePipe) {
    170          cdpConnection = await this.createCdpPipeConnection(browserProcess, {
    171            timeout,
    172            protocolTimeout,
    173            slowMo,
    174          });
    175        } else {
    176          cdpConnection = await this.createCdpSocketConnection(browserProcess, {
    177            timeout,
    178            protocolTimeout,
    179            slowMo,
    180          });
    181        }
    182        if (protocol === 'webDriverBiDi') {
    183          browser = await this.createBiDiOverCdpBrowser(
    184            browserProcess,
    185            cdpConnection,
    186            browserCloseCallback,
    187            {
    188              defaultViewport,
    189              acceptInsecureCerts,
    190            },
    191          );
    192        } else {
    193          browser = await CdpBrowser._create(
    194            cdpConnection,
    195            [],
    196            acceptInsecureCerts,
    197            defaultViewport,
    198            downloadBehavior,
    199            browserProcess.nodeProcess,
    200            browserCloseCallback,
    201            options.targetFilter,
    202          );
    203        }
    204      }
    205    } catch (error) {
    206      void browserCloseCallback();
    207      if (error instanceof BrowsersTimeoutError) {
    208        throw new TimeoutError(error.message);
    209      }
    210      throw error;
    211    }
    212 
    213    if (Array.isArray(enableExtensions)) {
    214      if (this.#browser === 'chrome' && !usePipe) {
    215        throw new Error(
    216          'To use `enableExtensions` with a list of paths in Chrome, you must be connected with `--remote-debugging-pipe` (`pipe: true`).',
    217        );
    218      }
    219 
    220      await Promise.all([
    221        enableExtensions.map(path => {
    222          return browser.installExtension(path);
    223        }),
    224      ]);
    225    }
    226 
    227    if (waitForInitialPage) {
    228      await this.waitForPageTarget(browser, timeout);
    229    }
    230 
    231    return browser;
    232  }
    233 
    234  abstract executablePath(
    235    channel?: ChromeReleaseChannel,
    236    validatePath?: boolean,
    237  ): string;
    238 
    239  abstract defaultArgs(object: LaunchOptions): string[];
    240 
    241  /**
    242   * @internal
    243   */
    244  protected abstract computeLaunchArguments(
    245    options: LaunchOptions,
    246  ): Promise<ResolvedLaunchArgs>;
    247 
    248  /**
    249   * @internal
    250   */
    251  protected abstract cleanUserDataDir(
    252    path: string,
    253    opts: {isTemp: boolean},
    254  ): Promise<void>;
    255 
    256  /**
    257   * @internal
    258   */
    259  protected async closeBrowser(
    260    browserProcess: ReturnType<typeof launch>,
    261    cdpConnection?: Connection,
    262  ): Promise<void> {
    263    if (cdpConnection) {
    264      // Attempt to close the browser gracefully
    265      try {
    266        await cdpConnection.closeBrowser();
    267        await browserProcess.hasClosed();
    268      } catch (error) {
    269        debugError(error);
    270        await browserProcess.close();
    271      }
    272    } else {
    273      // Wait for a possible graceful shutdown.
    274      await firstValueFrom(
    275        race(
    276          from(browserProcess.hasClosed()),
    277          timer(5000).pipe(
    278            map(() => {
    279              return from(browserProcess.close());
    280            }),
    281          ),
    282        ),
    283      );
    284    }
    285  }
    286 
    287  /**
    288   * @internal
    289   */
    290  protected async waitForPageTarget(
    291    browser: Browser,
    292    timeout: number,
    293  ): Promise<void> {
    294    try {
    295      await browser.waitForTarget(
    296        t => {
    297          return t.type() === 'page';
    298        },
    299        {timeout},
    300      );
    301    } catch (error) {
    302      await browser.close();
    303      throw error;
    304    }
    305  }
    306 
    307  /**
    308   * @internal
    309   */
    310  protected async createCdpSocketConnection(
    311    browserProcess: ReturnType<typeof launch>,
    312    opts: {
    313      timeout: number;
    314      protocolTimeout: number | undefined;
    315      slowMo: number;
    316    },
    317  ): Promise<Connection> {
    318    const browserWSEndpoint = await browserProcess.waitForLineOutput(
    319      CDP_WEBSOCKET_ENDPOINT_REGEX,
    320      opts.timeout,
    321    );
    322    const transport = await WebSocketTransport.create(browserWSEndpoint);
    323    return new Connection(
    324      browserWSEndpoint,
    325      transport,
    326      opts.slowMo,
    327      opts.protocolTimeout,
    328    );
    329  }
    330 
    331  /**
    332   * @internal
    333   */
    334  protected async createCdpPipeConnection(
    335    browserProcess: ReturnType<typeof launch>,
    336    opts: {
    337      timeout: number;
    338      protocolTimeout: number | undefined;
    339      slowMo: number;
    340    },
    341  ): Promise<Connection> {
    342    // stdio was assigned during start(), and the 'pipe' option there adds the
    343    // 4th and 5th items to stdio array
    344    const {3: pipeWrite, 4: pipeRead} = browserProcess.nodeProcess.stdio;
    345    const transport = new PipeTransport(
    346      pipeWrite as NodeJS.WritableStream,
    347      pipeRead as NodeJS.ReadableStream,
    348    );
    349    return new Connection('', transport, opts.slowMo, opts.protocolTimeout);
    350  }
    351 
    352  /**
    353   * @internal
    354   */
    355  protected async createBiDiOverCdpBrowser(
    356    browserProcess: ReturnType<typeof launch>,
    357    connection: Connection,
    358    closeCallback: BrowserCloseCallback,
    359    opts: {
    360      defaultViewport: Viewport | null;
    361      acceptInsecureCerts?: boolean;
    362    },
    363  ): Promise<Browser> {
    364    const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
    365    const bidiConnection = await BiDi.connectBidiOverCdp(connection);
    366    return await BiDi.BidiBrowser.create({
    367      connection: bidiConnection,
    368      cdpConnection: connection,
    369      closeCallback,
    370      process: browserProcess.nodeProcess,
    371      defaultViewport: opts.defaultViewport,
    372      acceptInsecureCerts: opts.acceptInsecureCerts,
    373    });
    374  }
    375 
    376  /**
    377   * @internal
    378   */
    379  protected async createBiDiBrowser(
    380    browserProcess: ReturnType<typeof launch>,
    381    closeCallback: BrowserCloseCallback,
    382    opts: {
    383      timeout: number;
    384      protocolTimeout: number | undefined;
    385      slowMo: number;
    386      defaultViewport: Viewport | null;
    387      acceptInsecureCerts?: boolean;
    388    },
    389  ): Promise<Browser> {
    390    const browserWSEndpoint =
    391      (await browserProcess.waitForLineOutput(
    392        WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
    393        opts.timeout,
    394      )) + '/session';
    395    const transport = await WebSocketTransport.create(browserWSEndpoint);
    396    const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
    397    const bidiConnection = new BiDi.BidiConnection(
    398      browserWSEndpoint,
    399      transport,
    400      opts.slowMo,
    401      opts.protocolTimeout,
    402    );
    403    return await BiDi.BidiBrowser.create({
    404      connection: bidiConnection,
    405      closeCallback,
    406      process: browserProcess.nodeProcess,
    407      defaultViewport: opts.defaultViewport,
    408      acceptInsecureCerts: opts.acceptInsecureCerts,
    409    });
    410  }
    411 
    412  /**
    413   * @internal
    414   */
    415  protected getProfilePath(): string {
    416    return join(
    417      this.puppeteer.configuration.temporaryDirectory ?? tmpdir(),
    418      `puppeteer_dev_${this.browser}_profile-`,
    419    );
    420  }
    421 
    422  /**
    423   * @internal
    424   */
    425  resolveExecutablePath(
    426    headless?: boolean | 'shell',
    427    validatePath = true,
    428  ): string {
    429    let executablePath = this.puppeteer.configuration.executablePath;
    430    if (executablePath) {
    431      if (validatePath && !existsSync(executablePath)) {
    432        throw new Error(
    433          `Tried to find the browser at the configured path (${executablePath}), but no executable was found.`,
    434        );
    435      }
    436      return executablePath;
    437    }
    438 
    439    function puppeteerBrowserToInstalledBrowser(
    440      browser?: SupportedBrowser,
    441      headless?: boolean | 'shell',
    442    ) {
    443      switch (browser) {
    444        case 'chrome':
    445          if (headless === 'shell') {
    446            return InstalledBrowser.CHROMEHEADLESSSHELL;
    447          }
    448          return InstalledBrowser.CHROME;
    449        case 'firefox':
    450          return InstalledBrowser.FIREFOX;
    451      }
    452      return InstalledBrowser.CHROME;
    453    }
    454 
    455    const browserType = puppeteerBrowserToInstalledBrowser(
    456      this.browser,
    457      headless,
    458    );
    459 
    460    executablePath = computeExecutablePath({
    461      cacheDir: this.puppeteer.defaultDownloadPath!,
    462      browser: browserType,
    463      buildId: this.puppeteer.browserVersion,
    464    });
    465 
    466    if (validatePath && !existsSync(executablePath)) {
    467      const configVersion =
    468        this.puppeteer.configuration?.[this.browser]?.version;
    469      if (configVersion) {
    470        throw new Error(
    471          `Tried to find the browser at the configured path (${executablePath}) for version ${configVersion}, but no executable was found.`,
    472        );
    473      }
    474      switch (this.browser) {
    475        case 'chrome':
    476          throw new Error(
    477            `Could not find Chrome (ver. ${this.puppeteer.browserVersion}). This can occur if either\n` +
    478              ` 1. you did not perform an installation before running the script (e.g. \`npx puppeteer browsers install ${browserType}\`) or\n` +
    479              ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
    480              'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.',
    481          );
    482        case 'firefox':
    483          throw new Error(
    484            `Could not find Firefox (rev. ${this.puppeteer.browserVersion}). This can occur if either\n` +
    485              ' 1. you did not perform an installation for Firefox before running the script (e.g. `npx puppeteer browsers install firefox`) or\n' +
    486              ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
    487              'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.',
    488          );
    489      }
    490    }
    491    return executablePath;
    492  }
    493 }