tor-browser

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

firefox.ts (15315B)


      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 path from 'node:path';
      9 
     10 import {getJSON} from '../httpUtil.js';
     11 
     12 import {BrowserPlatform, type ProfileOptions} from './types.js';
     13 
     14 function getFormat(buildId: string): string {
     15  const majorVersion = Number(buildId.split('.').shift()!);
     16  return majorVersion >= 135 ? 'xz' : 'bz2';
     17 }
     18 
     19 function archiveNightly(platform: BrowserPlatform, buildId: string): string {
     20  switch (platform) {
     21    case BrowserPlatform.LINUX:
     22      return `firefox-${buildId}.en-US.linux-x86_64.tar.${getFormat(buildId)}`;
     23    case BrowserPlatform.LINUX_ARM:
     24      return `firefox-${buildId}.en-US.linux-aarch64.tar.${getFormat(buildId)}`;
     25    case BrowserPlatform.MAC_ARM:
     26    case BrowserPlatform.MAC:
     27      return `firefox-${buildId}.en-US.mac.dmg`;
     28    case BrowserPlatform.WIN32:
     29    case BrowserPlatform.WIN64:
     30      return `firefox-${buildId}.en-US.${platform}.zip`;
     31  }
     32 }
     33 
     34 function archive(platform: BrowserPlatform, buildId: string): string {
     35  switch (platform) {
     36    case BrowserPlatform.LINUX_ARM:
     37    case BrowserPlatform.LINUX:
     38      return `firefox-${buildId}.tar.${getFormat(buildId)}`;
     39    case BrowserPlatform.MAC_ARM:
     40    case BrowserPlatform.MAC:
     41      return `Firefox ${buildId}.dmg`;
     42    case BrowserPlatform.WIN32:
     43    case BrowserPlatform.WIN64:
     44      return `Firefox Setup ${buildId}.exe`;
     45  }
     46 }
     47 
     48 function platformName(platform: BrowserPlatform): string {
     49  switch (platform) {
     50    case BrowserPlatform.LINUX:
     51      return `linux-x86_64`;
     52    case BrowserPlatform.LINUX_ARM:
     53      return `linux-aarch64`;
     54    case BrowserPlatform.MAC_ARM:
     55    case BrowserPlatform.MAC:
     56      return `mac`;
     57    case BrowserPlatform.WIN32:
     58    case BrowserPlatform.WIN64:
     59      return platform;
     60  }
     61 }
     62 
     63 function parseBuildId(buildId: string): [FirefoxChannel, string] {
     64  for (const value of Object.values(FirefoxChannel)) {
     65    if (buildId.startsWith(value + '_')) {
     66      buildId = buildId.substring(value.length + 1);
     67      return [value, buildId];
     68    }
     69  }
     70  // Older versions do not have channel as the prefix.«
     71  return [FirefoxChannel.NIGHTLY, buildId];
     72 }
     73 
     74 export function resolveDownloadUrl(
     75  platform: BrowserPlatform,
     76  buildId: string,
     77  baseUrl?: string,
     78 ): string {
     79  const [channel] = parseBuildId(buildId);
     80  switch (channel) {
     81    case FirefoxChannel.NIGHTLY:
     82      baseUrl ??=
     83        'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central';
     84      break;
     85    case FirefoxChannel.DEVEDITION:
     86      baseUrl ??= 'https://archive.mozilla.org/pub/devedition/releases';
     87      break;
     88    case FirefoxChannel.BETA:
     89    case FirefoxChannel.STABLE:
     90    case FirefoxChannel.ESR:
     91      baseUrl ??= 'https://archive.mozilla.org/pub/firefox/releases';
     92      break;
     93  }
     94  return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
     95 }
     96 
     97 export function resolveDownloadPath(
     98  platform: BrowserPlatform,
     99  buildId: string,
    100 ): string[] {
    101  const [channel, resolvedBuildId] = parseBuildId(buildId);
    102  switch (channel) {
    103    case FirefoxChannel.NIGHTLY:
    104      return [archiveNightly(platform, resolvedBuildId)];
    105    case FirefoxChannel.DEVEDITION:
    106    case FirefoxChannel.BETA:
    107    case FirefoxChannel.STABLE:
    108    case FirefoxChannel.ESR:
    109      return [
    110        resolvedBuildId,
    111        platformName(platform),
    112        'en-US',
    113        archive(platform, resolvedBuildId),
    114      ];
    115  }
    116 }
    117 
    118 export function relativeExecutablePath(
    119  platform: BrowserPlatform,
    120  buildId: string,
    121 ): string {
    122  const [channel] = parseBuildId(buildId);
    123  switch (channel) {
    124    case FirefoxChannel.NIGHTLY:
    125      switch (platform) {
    126        case BrowserPlatform.MAC_ARM:
    127        case BrowserPlatform.MAC:
    128          return path.join(
    129            'Firefox Nightly.app',
    130            'Contents',
    131            'MacOS',
    132            'firefox',
    133          );
    134        case BrowserPlatform.LINUX_ARM:
    135        case BrowserPlatform.LINUX:
    136          return path.join('firefox', 'firefox');
    137        case BrowserPlatform.WIN32:
    138        case BrowserPlatform.WIN64:
    139          return path.join('firefox', 'firefox.exe');
    140      }
    141    case FirefoxChannel.BETA:
    142    case FirefoxChannel.DEVEDITION:
    143    case FirefoxChannel.ESR:
    144    case FirefoxChannel.STABLE:
    145      switch (platform) {
    146        case BrowserPlatform.MAC_ARM:
    147        case BrowserPlatform.MAC:
    148          return path.join('Firefox.app', 'Contents', 'MacOS', 'firefox');
    149        case BrowserPlatform.LINUX_ARM:
    150        case BrowserPlatform.LINUX:
    151          return path.join('firefox', 'firefox');
    152        case BrowserPlatform.WIN32:
    153        case BrowserPlatform.WIN64:
    154          return path.join('core', 'firefox.exe');
    155      }
    156  }
    157 }
    158 
    159 export enum FirefoxChannel {
    160  STABLE = 'stable',
    161  ESR = 'esr',
    162  DEVEDITION = 'devedition',
    163  BETA = 'beta',
    164  NIGHTLY = 'nightly',
    165 }
    166 
    167 export async function resolveBuildId(
    168  channel: FirefoxChannel = FirefoxChannel.NIGHTLY,
    169 ): Promise<string> {
    170  const channelToVersionKey = {
    171    [FirefoxChannel.ESR]: 'FIREFOX_ESR',
    172    [FirefoxChannel.STABLE]: 'LATEST_FIREFOX_VERSION',
    173    [FirefoxChannel.DEVEDITION]: 'FIREFOX_DEVEDITION',
    174    [FirefoxChannel.BETA]: 'FIREFOX_DEVEDITION',
    175    [FirefoxChannel.NIGHTLY]: 'FIREFOX_NIGHTLY',
    176  };
    177  const versions = (await getJSON(
    178    new URL('https://product-details.mozilla.org/1.0/firefox_versions.json'),
    179  )) as Record<string, string>;
    180  const version = versions[channelToVersionKey[channel]];
    181  if (!version) {
    182    throw new Error(`Channel ${channel} is not found.`);
    183  }
    184  return channel + '_' + version;
    185 }
    186 
    187 export async function createProfile(options: ProfileOptions): Promise<void> {
    188  if (!fs.existsSync(options.path)) {
    189    await fs.promises.mkdir(options.path, {
    190      recursive: true,
    191    });
    192  }
    193  await syncPreferences({
    194    preferences: {
    195      ...defaultProfilePreferences(options.preferences),
    196      ...options.preferences,
    197    },
    198    path: options.path,
    199  });
    200 }
    201 
    202 function defaultProfilePreferences(
    203  extraPrefs: Record<string, unknown>,
    204 ): Record<string, unknown> {
    205  const server = 'dummy.test';
    206 
    207  const defaultPrefs = {
    208    // Make sure Shield doesn't hit the network.
    209    'app.normandy.api_url': '',
    210    // Disable Firefox old build background check
    211    'app.update.checkInstallTime': false,
    212    // Disable automatically upgrading Firefox
    213    'app.update.disabledForTesting': true,
    214 
    215    // Increase the APZ content response timeout to 1 minute
    216    'apz.content_response_timeout': 60000,
    217 
    218    // Prevent various error message on the console
    219    // jest-puppeteer asserts that no error message is emitted by the console
    220    'browser.contentblocking.features.standard':
    221      '-tp,tpPrivate,cookieBehavior0,-cryptoTP,-fp',
    222 
    223    // Enable the dump function: which sends messages to the system
    224    // console
    225    // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
    226    'browser.dom.window.dump.enabled': true,
    227    // Disable topstories
    228    'browser.newtabpage.activity-stream.feeds.system.topstories': false,
    229    // Always display a blank page
    230    'browser.newtabpage.enabled': false,
    231    // Background thumbnails in particular cause grief: and disabling
    232    // thumbnails in general cannot hurt
    233    'browser.pagethumbnails.capturing_disabled': true,
    234 
    235    // Disable safebrowsing components.
    236    'browser.safebrowsing.blockedURIs.enabled': false,
    237    'browser.safebrowsing.downloads.enabled': false,
    238    'browser.safebrowsing.malware.enabled': false,
    239    'browser.safebrowsing.phishing.enabled': false,
    240 
    241    // Disable updates to search engines.
    242    'browser.search.update': false,
    243    // Do not restore the last open set of tabs if the browser has crashed
    244    'browser.sessionstore.resume_from_crash': false,
    245    // Skip check for default browser on startup
    246    'browser.shell.checkDefaultBrowser': false,
    247 
    248    // Disable newtabpage
    249    'browser.startup.homepage': 'about:blank',
    250    // Do not redirect user when a milstone upgrade of Firefox is detected
    251    'browser.startup.homepage_override.mstone': 'ignore',
    252    // Start with a blank page about:blank
    253    'browser.startup.page': 0,
    254 
    255    // Do not allow background tabs to be zombified on Android: otherwise for
    256    // tests that open additional tabs: the test harness tab itself might get
    257    // unloaded
    258    'browser.tabs.disableBackgroundZombification': false,
    259    // Do not warn when closing all other open tabs
    260    'browser.tabs.warnOnCloseOtherTabs': false,
    261    // Do not warn when multiple tabs will be opened
    262    'browser.tabs.warnOnOpen': false,
    263 
    264    // Do not automatically offer translations, as tests do not expect this.
    265    'browser.translations.automaticallyPopup': false,
    266 
    267    // Disable the UI tour.
    268    'browser.uitour.enabled': false,
    269    // Turn off search suggestions in the location bar so as not to trigger
    270    // network connections.
    271    'browser.urlbar.suggest.searches': false,
    272    // Disable first run splash page on Windows 10
    273    'browser.usedOnWindows10.introURL': '',
    274    // Do not warn on quitting Firefox
    275    'browser.warnOnQuit': false,
    276 
    277    // Defensively disable data reporting systems
    278    'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
    279    'datareporting.healthreport.logging.consoleEnabled': false,
    280    'datareporting.healthreport.service.enabled': false,
    281    'datareporting.healthreport.service.firstRun': false,
    282    'datareporting.healthreport.uploadEnabled': false,
    283 
    284    // Do not show datareporting policy notifications which can interfere with tests
    285    'datareporting.policy.dataSubmissionEnabled': false,
    286    'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
    287 
    288    // DevTools JSONViewer sometimes fails to load dependencies with its require.js.
    289    // This doesn't affect Puppeteer but spams console (Bug 1424372)
    290    'devtools.jsonview.enabled': false,
    291 
    292    // Disable popup-blocker
    293    'dom.disable_open_during_load': false,
    294 
    295    // Enable the support for File object creation in the content process
    296    // Required for |Page.setFileInputFiles| protocol method.
    297    'dom.file.createInChild': true,
    298 
    299    // Disable the ProcessHangMonitor
    300    'dom.ipc.reportProcessHangs': false,
    301 
    302    // Disable slow script dialogues
    303    'dom.max_chrome_script_run_time': 0,
    304    'dom.max_script_run_time': 0,
    305 
    306    // Only load extensions from the application and user profile
    307    // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
    308    'extensions.autoDisableScopes': 0,
    309    'extensions.enabledScopes': 5,
    310 
    311    // Disable metadata caching for installed add-ons by default
    312    'extensions.getAddons.cache.enabled': false,
    313 
    314    // Disable installing any distribution extensions or add-ons.
    315    'extensions.installDistroAddons': false,
    316 
    317    // Turn off extension updates so they do not bother tests
    318    'extensions.update.enabled': false,
    319 
    320    // Turn off extension updates so they do not bother tests
    321    'extensions.update.notifyUser': false,
    322 
    323    // Make sure opening about:addons will not hit the network
    324    'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
    325 
    326    // Allow the application to have focus even it runs in the background
    327    'focusmanager.testmode': true,
    328 
    329    // Disable useragent updates
    330    'general.useragent.updates.enabled': false,
    331 
    332    // Always use network provider for geolocation tests so we bypass the
    333    // macOS dialog raised by the corelocation provider
    334    'geo.provider.testing': true,
    335 
    336    // Do not scan Wifi
    337    'geo.wifi.scan': false,
    338 
    339    // No hang monitor
    340    'hangmonitor.timeout': 0,
    341 
    342    // Show chrome errors and warnings in the error console
    343    'javascript.options.showInConsole': true,
    344 
    345    // Disable download and usage of OpenH264: and Widevine plugins
    346    'media.gmp-manager.updateEnabled': false,
    347 
    348    // Disable the GFX sanity window
    349    'media.sanity-test.disabled': true,
    350 
    351    // Disable experimental feature that is only available in Nightly
    352    'network.cookie.sameSite.laxByDefault': false,
    353 
    354    // Do not prompt for temporary redirects
    355    'network.http.prompt-temp-redirect': false,
    356 
    357    // Disable speculative connections so they are not reported as leaking
    358    // when they are hanging around
    359    'network.http.speculative-parallel-limit': 0,
    360 
    361    // Do not automatically switch between offline and online
    362    'network.manage-offline-status': false,
    363 
    364    // Make sure SNTP requests do not hit the network
    365    'network.sntp.pools': server,
    366 
    367    // Disable Flash.
    368    'plugin.state.flash': 0,
    369 
    370    'privacy.trackingprotection.enabled': false,
    371 
    372    // Can be removed once Firefox 89 is no longer supported
    373    // https://bugzilla.mozilla.org/show_bug.cgi?id=1710839
    374    'remote.enabled': true,
    375 
    376    // Disabled screenshots component
    377    'screenshots.browser.component.enabled': false,
    378 
    379    // Don't do network connections for mitm priming
    380    'security.certerrors.mitm.priming.enabled': false,
    381 
    382    // Local documents have access to all other local documents,
    383    // including directory listings
    384    'security.fileuri.strict_origin_policy': false,
    385 
    386    // Do not wait for the notification button security delay
    387    'security.notification_enable_delay': 0,
    388 
    389    // Ensure blocklist updates do not hit the network
    390    'services.settings.server': `http://${server}/dummy/blocklist/`,
    391 
    392    // Do not automatically fill sign-in forms with known usernames and
    393    // passwords
    394    'signon.autofillForms': false,
    395 
    396    // Disable password capture, so that tests that include forms are not
    397    // influenced by the presence of the persistent doorhanger notification
    398    'signon.rememberSignons': false,
    399 
    400    // Disable first-run welcome page
    401    'startup.homepage_welcome_url': 'about:blank',
    402 
    403    // Disable first-run welcome page
    404    'startup.homepage_welcome_url.additional': '',
    405 
    406    // Disable browser animations (tabs, fullscreen, sliding alerts)
    407    'toolkit.cosmeticAnimations.enabled': false,
    408 
    409    // Prevent starting into safe mode after application crashes
    410    'toolkit.startup.max_resumed_crashes': -1,
    411  };
    412 
    413  return Object.assign(defaultPrefs, extraPrefs);
    414 }
    415 
    416 async function backupFile(input: string): Promise<void> {
    417  if (!fs.existsSync(input)) {
    418    return;
    419  }
    420  await fs.promises.copyFile(input, input + '.puppeteer');
    421 }
    422 
    423 /**
    424 * Populates the user.js file with custom preferences as needed to allow
    425 * Firefox's support to properly function. These preferences will be
    426 * automatically copied over to prefs.js during startup of Firefox. To be
    427 * able to restore the original values of preferences a backup of prefs.js
    428 * will be created.
    429 */
    430 async function syncPreferences(options: ProfileOptions): Promise<void> {
    431  const prefsPath = path.join(options.path, 'prefs.js');
    432  const userPath = path.join(options.path, 'user.js');
    433 
    434  const lines = Object.entries(options.preferences).map(([key, value]) => {
    435    return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
    436  });
    437 
    438  // Use allSettled to prevent corruption.
    439  const result = await Promise.allSettled([
    440    backupFile(userPath).then(async () => {
    441      await fs.promises.writeFile(userPath, lines.join('\n'));
    442    }),
    443    backupFile(prefsPath),
    444  ]);
    445  for (const command of result) {
    446    if (command.status === 'rejected') {
    447      throw command.reason;
    448    }
    449  }
    450 }
    451 
    452 export function compareVersions(a: string, b: string): number {
    453  // TODO: this is a not very reliable check.
    454  return parseInt(a.replace('.', ''), 16) - parseInt(b.replace('.', ''), 16);
    455 }