tor-browser

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

ChromeLauncher.ts (9876B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import {mkdtemp} from 'node:fs/promises';
      8 import os from 'node:os';
      9 import path from 'node:path';
     10 
     11 import {
     12  computeSystemExecutablePath,
     13  Browser as SupportedBrowsers,
     14  ChromeReleaseChannel as BrowsersChromeReleaseChannel,
     15 } from '@puppeteer/browsers';
     16 
     17 import type {Browser} from '../api/Browser.js';
     18 import {debugError} from '../common/util.js';
     19 import {assert} from '../util/assert.js';
     20 
     21 import {BrowserLauncher, type ResolvedLaunchArgs} from './BrowserLauncher.js';
     22 import type {ChromeReleaseChannel, LaunchOptions} from './LaunchOptions.js';
     23 import type {PuppeteerNode} from './PuppeteerNode.js';
     24 import {rm} from './util/fs.js';
     25 
     26 /**
     27 * @internal
     28 */
     29 export class ChromeLauncher extends BrowserLauncher {
     30  constructor(puppeteer: PuppeteerNode) {
     31    super(puppeteer, 'chrome');
     32  }
     33 
     34  override launch(options: LaunchOptions = {}): Promise<Browser> {
     35    if (
     36      this.puppeteer.configuration.logLevel === 'warn' &&
     37      process.platform === 'darwin' &&
     38      process.arch === 'x64'
     39    ) {
     40      const cpus = os.cpus();
     41      if (cpus[0]?.model.includes('Apple')) {
     42        console.warn(
     43          [
     44            '\x1B[1m\x1B[43m\x1B[30m',
     45            'Degraded performance warning:\x1B[0m\x1B[33m',
     46            'Launching Chrome on Mac Silicon (arm64) from an x64 Node installation results in',
     47            'Rosetta translating the Chrome binary, even if Chrome is already arm64. This would',
     48            'result in huge performance issues. To resolve this, you must run Puppeteer with',
     49            'a version of Node built for arm64.',
     50          ].join('\n  '),
     51        );
     52      }
     53    }
     54 
     55    return super.launch(options);
     56  }
     57 
     58  /**
     59   * @internal
     60   */
     61  override async computeLaunchArguments(
     62    options: LaunchOptions = {},
     63  ): Promise<ResolvedLaunchArgs> {
     64    const {
     65      ignoreDefaultArgs = false,
     66      args = [],
     67      pipe = false,
     68      debuggingPort,
     69      channel,
     70      executablePath,
     71    } = options;
     72 
     73    const chromeArguments = [];
     74    if (!ignoreDefaultArgs) {
     75      chromeArguments.push(...this.defaultArgs(options));
     76    } else if (Array.isArray(ignoreDefaultArgs)) {
     77      chromeArguments.push(
     78        ...this.defaultArgs(options).filter(arg => {
     79          return !ignoreDefaultArgs.includes(arg);
     80        }),
     81      );
     82    } else {
     83      chromeArguments.push(...args);
     84    }
     85 
     86    if (
     87      !chromeArguments.some(argument => {
     88        return argument.startsWith('--remote-debugging-');
     89      })
     90    ) {
     91      if (pipe) {
     92        assert(
     93          !debuggingPort,
     94          'Browser should be launched with either pipe or debugging port - not both.',
     95        );
     96        chromeArguments.push('--remote-debugging-pipe');
     97      } else {
     98        chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
     99      }
    100    }
    101 
    102    let isTempUserDataDir = false;
    103 
    104    // Check for the user data dir argument, which will always be set even
    105    // with a custom directory specified via the userDataDir option.
    106    let userDataDirIndex = chromeArguments.findIndex(arg => {
    107      return arg.startsWith('--user-data-dir');
    108    });
    109    if (userDataDirIndex < 0) {
    110      isTempUserDataDir = true;
    111      chromeArguments.push(
    112        `--user-data-dir=${await mkdtemp(this.getProfilePath())}`,
    113      );
    114      userDataDirIndex = chromeArguments.length - 1;
    115    }
    116 
    117    const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1];
    118    assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed');
    119 
    120    let chromeExecutable = executablePath;
    121    if (!chromeExecutable) {
    122      assert(
    123        channel || !this.puppeteer._isPuppeteerCore,
    124        `An \`executablePath\` or \`channel\` must be specified for \`puppeteer-core\``,
    125      );
    126      chromeExecutable = channel
    127        ? this.executablePath(channel)
    128        : this.resolveExecutablePath(options.headless ?? true);
    129    }
    130 
    131    return {
    132      executablePath: chromeExecutable,
    133      args: chromeArguments,
    134      isTempUserDataDir,
    135      userDataDir,
    136    };
    137  }
    138 
    139  /**
    140   * @internal
    141   */
    142  override async cleanUserDataDir(
    143    path: string,
    144    opts: {isTemp: boolean},
    145  ): Promise<void> {
    146    if (opts.isTemp) {
    147      try {
    148        await rm(path);
    149      } catch (error) {
    150        debugError(error);
    151        throw error;
    152      }
    153    }
    154  }
    155 
    156  override defaultArgs(options: LaunchOptions = {}): string[] {
    157    // See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
    158 
    159    const userDisabledFeatures = getFeatures(
    160      '--disable-features',
    161      options.args,
    162    );
    163    if (options.args && userDisabledFeatures.length > 0) {
    164      removeMatchingFlags(options.args, '--disable-features');
    165    }
    166 
    167    const turnOnExperimentalFeaturesForTesting =
    168      process.env['PUPPETEER_TEST_EXPERIMENTAL_CHROME_FEATURES'] === 'true';
    169 
    170    // Merge default disabled features with user-provided ones, if any.
    171    const disabledFeatures = [
    172      'Translate',
    173      // AcceptCHFrame disabled because of crbug.com/1348106.
    174      'AcceptCHFrame',
    175      'MediaRouter',
    176      'OptimizationHints',
    177      ...(turnOnExperimentalFeaturesForTesting
    178        ? []
    179        : [
    180            // https://crbug.com/1492053
    181            'ProcessPerSiteUpToMainFrameThreshold',
    182            // https://github.com/puppeteer/puppeteer/issues/10715
    183            'IsolateSandboxedIframes',
    184          ]),
    185      ...userDisabledFeatures,
    186    ].filter(feature => {
    187      return feature !== '';
    188    });
    189 
    190    const userEnabledFeatures = getFeatures('--enable-features', options.args);
    191    if (options.args && userEnabledFeatures.length > 0) {
    192      removeMatchingFlags(options.args, '--enable-features');
    193    }
    194 
    195    // Merge default enabled features with user-provided ones, if any.
    196    const enabledFeatures = [
    197      'PdfOopif',
    198      // Add features to enable by default here.
    199      ...userEnabledFeatures,
    200    ].filter(feature => {
    201      return feature !== '';
    202    });
    203 
    204    const chromeArguments = [
    205      '--allow-pre-commit-input',
    206      '--disable-background-networking',
    207      '--disable-background-timer-throttling',
    208      '--disable-backgrounding-occluded-windows',
    209      '--disable-breakpad',
    210      '--disable-client-side-phishing-detection',
    211      '--disable-component-extensions-with-background-pages',
    212      '--disable-crash-reporter', // No crash reporting in CfT.
    213      '--disable-default-apps',
    214      '--disable-dev-shm-usage',
    215      '--disable-hang-monitor',
    216      '--disable-infobars',
    217      '--disable-ipc-flooding-protection',
    218      '--disable-popup-blocking',
    219      '--disable-prompt-on-repost',
    220      '--disable-renderer-backgrounding',
    221      '--disable-search-engine-choice-screen',
    222      '--disable-sync',
    223      '--enable-automation',
    224      '--export-tagged-pdf',
    225      '--force-color-profile=srgb',
    226      '--generate-pdf-document-outline',
    227      '--metrics-recording-only',
    228      '--no-first-run',
    229      '--password-store=basic',
    230      '--use-mock-keychain',
    231      `--disable-features=${disabledFeatures.join(',')}`,
    232      `--enable-features=${enabledFeatures.join(',')}`,
    233    ].filter(arg => {
    234      return arg !== '';
    235    });
    236    const {
    237      devtools = false,
    238      headless = !devtools,
    239      args = [],
    240      userDataDir,
    241      enableExtensions = false,
    242    } = options;
    243    if (userDataDir) {
    244      chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
    245    }
    246    if (devtools) {
    247      chromeArguments.push('--auto-open-devtools-for-tabs');
    248    }
    249    if (headless) {
    250      chromeArguments.push(
    251        headless === 'shell' ? '--headless' : '--headless=new',
    252        '--hide-scrollbars',
    253        '--mute-audio',
    254      );
    255    }
    256    chromeArguments.push(
    257      enableExtensions
    258        ? '--enable-unsafe-extension-debugging'
    259        : '--disable-extensions',
    260    );
    261    if (
    262      args.every(arg => {
    263        return arg.startsWith('-');
    264      })
    265    ) {
    266      chromeArguments.push('about:blank');
    267    }
    268    chromeArguments.push(...args);
    269    return chromeArguments;
    270  }
    271 
    272  override executablePath(
    273    channel?: ChromeReleaseChannel,
    274    validatePath = true,
    275  ): string {
    276    if (channel) {
    277      return computeSystemExecutablePath({
    278        browser: SupportedBrowsers.CHROME,
    279        channel: convertPuppeteerChannelToBrowsersChannel(channel),
    280      });
    281    } else {
    282      return this.resolveExecutablePath(undefined, validatePath);
    283    }
    284  }
    285 }
    286 
    287 function convertPuppeteerChannelToBrowsersChannel(
    288  channel: ChromeReleaseChannel,
    289 ): BrowsersChromeReleaseChannel {
    290  switch (channel) {
    291    case 'chrome':
    292      return BrowsersChromeReleaseChannel.STABLE;
    293    case 'chrome-dev':
    294      return BrowsersChromeReleaseChannel.DEV;
    295    case 'chrome-beta':
    296      return BrowsersChromeReleaseChannel.BETA;
    297    case 'chrome-canary':
    298      return BrowsersChromeReleaseChannel.CANARY;
    299  }
    300 }
    301 
    302 /**
    303 * Extracts all features from the given command-line flag
    304 * (e.g. `--enable-features`, `--enable-features=`).
    305 *
    306 * Example input:
    307 * ["--enable-features=NetworkService,NetworkServiceInProcess", "--enable-features=Foo"]
    308 *
    309 * Example output:
    310 * ["NetworkService", "NetworkServiceInProcess", "Foo"]
    311 *
    312 * @internal
    313 */
    314 export function getFeatures(flag: string, options: string[] = []): string[] {
    315  return options
    316    .filter(s => {
    317      return s.startsWith(flag.endsWith('=') ? flag : `${flag}=`);
    318    })
    319    .map(s => {
    320      return s.split(new RegExp(`${flag}=\\s*`))[1]?.trim();
    321    })
    322    .filter(s => {
    323      return s;
    324    }) as string[];
    325 }
    326 
    327 /**
    328 * Removes all elements in-place from the given string array
    329 * that match the given command-line flag.
    330 *
    331 * @internal
    332 */
    333 export function removeMatchingFlags(array: string[], flag: string): string[] {
    334  const regex = new RegExp(`^${flag}=.*`);
    335  let i = 0;
    336  while (i < array.length) {
    337    if (regex.test(array[i]!)) {
    338      array.splice(i, 1);
    339    } else {
    340      i++;
    341    }
    342  }
    343  return array;
    344 }