tor-browser

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

FirefoxLauncher.ts (5885B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import fs from 'node:fs';
      8 import {rename, unlink, mkdtemp} from 'node:fs/promises';
      9 import os from 'node:os';
     10 import path from 'node:path';
     11 
     12 import {Browser as SupportedBrowsers, createProfile} from '@puppeteer/browsers';
     13 
     14 import {debugError} from '../common/util.js';
     15 import {assert} from '../util/assert.js';
     16 
     17 import {BrowserLauncher, type ResolvedLaunchArgs} from './BrowserLauncher.js';
     18 import type {LaunchOptions} from './LaunchOptions.js';
     19 import type {PuppeteerNode} from './PuppeteerNode.js';
     20 import {rm} from './util/fs.js';
     21 
     22 /**
     23 * @internal
     24 */
     25 export class FirefoxLauncher extends BrowserLauncher {
     26  constructor(puppeteer: PuppeteerNode) {
     27    super(puppeteer, 'firefox');
     28  }
     29 
     30  static getPreferences(
     31    extraPrefsFirefox?: Record<string, unknown>,
     32  ): Record<string, unknown> {
     33    return {
     34      ...extraPrefsFirefox,
     35      // Force all web content to use a single content process. TODO: remove
     36      // this once Firefox supports mouse event dispatch from the main frame
     37      // context. See https://bugzilla.mozilla.org/show_bug.cgi?id=1773393.
     38      'fission.webContentIsolationStrategy': 0,
     39    };
     40  }
     41 
     42  /**
     43   * @internal
     44   */
     45  override async computeLaunchArguments(
     46    options: LaunchOptions = {},
     47  ): Promise<ResolvedLaunchArgs> {
     48    const {
     49      ignoreDefaultArgs = false,
     50      args = [],
     51      executablePath,
     52      pipe = false,
     53      extraPrefsFirefox = {},
     54      debuggingPort = null,
     55    } = options;
     56 
     57    const firefoxArguments = [];
     58    if (!ignoreDefaultArgs) {
     59      firefoxArguments.push(...this.defaultArgs(options));
     60    } else if (Array.isArray(ignoreDefaultArgs)) {
     61      firefoxArguments.push(
     62        ...this.defaultArgs(options).filter(arg => {
     63          return !ignoreDefaultArgs.includes(arg);
     64        }),
     65      );
     66    } else {
     67      firefoxArguments.push(...args);
     68    }
     69 
     70    if (
     71      !firefoxArguments.some(argument => {
     72        return argument.startsWith('--remote-debugging-');
     73      })
     74    ) {
     75      if (pipe) {
     76        assert(
     77          debuggingPort === null,
     78          'Browser should be launched with either pipe or debugging port - not both.',
     79        );
     80      }
     81      firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
     82    }
     83 
     84    let userDataDir: string | undefined;
     85    let isTempUserDataDir = true;
     86 
     87    // Check for the profile argument, which will always be set even
     88    // with a custom directory specified via the userDataDir option.
     89    const profileArgIndex = firefoxArguments.findIndex(arg => {
     90      return ['-profile', '--profile'].includes(arg);
     91    });
     92 
     93    if (profileArgIndex !== -1) {
     94      userDataDir = firefoxArguments[profileArgIndex + 1];
     95      if (!userDataDir) {
     96        throw new Error(`Missing value for profile command line argument`);
     97      }
     98 
     99      // When using a custom Firefox profile it needs to be populated
    100      // with required preferences.
    101      isTempUserDataDir = false;
    102    } else {
    103      userDataDir = await mkdtemp(this.getProfilePath());
    104      firefoxArguments.push('--profile');
    105      firefoxArguments.push(userDataDir);
    106    }
    107 
    108    await createProfile(SupportedBrowsers.FIREFOX, {
    109      path: userDataDir,
    110      preferences: FirefoxLauncher.getPreferences(extraPrefsFirefox),
    111    });
    112 
    113    let firefoxExecutable: string;
    114    if (this.puppeteer._isPuppeteerCore || executablePath) {
    115      assert(
    116        executablePath,
    117        `An \`executablePath\` must be specified for \`puppeteer-core\``,
    118      );
    119      firefoxExecutable = executablePath;
    120    } else {
    121      firefoxExecutable = this.executablePath(undefined);
    122    }
    123 
    124    return {
    125      isTempUserDataDir,
    126      userDataDir,
    127      args: firefoxArguments,
    128      executablePath: firefoxExecutable,
    129    };
    130  }
    131 
    132  /**
    133   * @internal
    134   */
    135  override async cleanUserDataDir(
    136    userDataDir: string,
    137    opts: {isTemp: boolean},
    138  ): Promise<void> {
    139    if (opts.isTemp) {
    140      try {
    141        await rm(userDataDir);
    142      } catch (error) {
    143        debugError(error);
    144        throw error;
    145      }
    146    } else {
    147      try {
    148        const backupSuffix = '.puppeteer';
    149        const backupFiles = ['prefs.js', 'user.js'];
    150 
    151        const results = await Promise.allSettled(
    152          backupFiles.map(async file => {
    153            const prefsBackupPath = path.join(userDataDir, file + backupSuffix);
    154            if (fs.existsSync(prefsBackupPath)) {
    155              const prefsPath = path.join(userDataDir, file);
    156              await unlink(prefsPath);
    157              await rename(prefsBackupPath, prefsPath);
    158            }
    159          }),
    160        );
    161        for (const result of results) {
    162          if (result.status === 'rejected') {
    163            throw result.reason;
    164          }
    165        }
    166      } catch (error) {
    167        debugError(error);
    168      }
    169    }
    170  }
    171 
    172  override executablePath(_: unknown, validatePath = true): string {
    173    return this.resolveExecutablePath(
    174      undefined,
    175      /* validatePath=*/ validatePath,
    176    );
    177  }
    178 
    179  override defaultArgs(options: LaunchOptions = {}): string[] {
    180    const {
    181      devtools = false,
    182      headless = !devtools,
    183      args = [],
    184      userDataDir = null,
    185    } = options;
    186 
    187    const firefoxArguments = [];
    188 
    189    switch (os.platform()) {
    190      case 'darwin':
    191        firefoxArguments.push('--foreground');
    192        break;
    193      case 'win32':
    194        firefoxArguments.push('--wait-for-browser');
    195        break;
    196    }
    197    if (userDataDir) {
    198      firefoxArguments.push('--profile');
    199      firefoxArguments.push(userDataDir);
    200    }
    201    if (headless) {
    202      firefoxArguments.push('--headless');
    203    }
    204    if (devtools) {
    205      firefoxArguments.push('--devtools');
    206    }
    207    if (
    208      args.every(arg => {
    209        return arg.startsWith('-');
    210      })
    211    ) {
    212      firefoxArguments.push('about:blank');
    213    }
    214    firefoxArguments.push(...args);
    215    return firefoxArguments;
    216  }
    217 }