import * as _ from 'lodash' ; import * as path from 'path' ; import { SpawnOptions } from 'child_process' ; import { APP_ROOT } from '../constants' ; import { HtkConfig } from '../config' ; import { reportError } from '../error-tracking' ; import { getAvailableBrowsers , launchBrowser , BrowserInstance } from '../browsers' ; import { delay } from '../util/promise' ; import { isErrorLike } from '../util/error' ; import { readFile , canAccess , deleteFolder } from '../util/fs' ; import { windowsKill , spawnToResult } from '../util/process-management' ; import { MessageServer } from '../message-server' ; import { CertCheckServer } from '../cert-check-server' ; import { Interceptor } from '.' ; const FIREFOX_PREF_REGEX = /\w+_pref\("([^"]+)", (.*)\);/ ; let profileSetupBrowser ; let browsers = { } ; export const NSS_DIR = path . join ( APP_ROOT , 'nss' ) ; const testCertutil = ( command , options ? ) => { return spawnToResult ( command , [ '-h' ] , options ) . then ( ( output ) => output . exitCode === 1 && output . stderr . includes ( "Utility to manipulate NSS certificate databases" ) ) . catch ( ( e ) => { if ( ! isErrorLike ( e ) || e . code !== 'ENOENT' ) { console . log ( `Failed to run ${ command }` ) ; console . log ( e ) ; } return false ; } ) ; } ; const getCertutilCommand = _ . memoize ( async ( ) => { if ( await testCertutil ( 'certutil' ) ) return { command : 'certutil' } ; const bundledCertUtil = path . join ( NSS_DIR , process . platform , 'certutil' ) ; if ( process . platform !== 'linux' ) { if ( await testCertutil ( bundledCertUtil ) ) { return { command : bundledCertUtil } ; } else { throw new Error ( "No certutil available" ) ; } } const certutilEnv = { ... process . env , LD_LIBRARY_PATH : process . env . LD_LIBRARY_PATH ? `${ path . join ( NSS_DIR , process . platform ) }:${ process . env . LD_LIBRARY_PATH }` : path . join ( NSS_DIR , process . platform ) } ; if ( await testCertutil ( bundledCertUtil , { env : certutilEnv } ) ) { return { command : bundledCertUtil , options : { env : certutilEnv } } ; } else { throw new Error ( "No certutil available" ) ; } } ) ; export class FreshFirefox implements Interceptor { id = 'fresh-firefox' ; version = '1.1.0' ; constructor ( private config ) { } private readonly firefoxProfilePath = path . join ( this . config . configPath , 'firefox-profile' ) ; isActive ( proxyPort ) { return browsers [ proxyPort ] != null && ! ! browsers [ proxyPort ] . pid ; } async isActivable ( ) { const availableBrowsers = await getAvailableBrowsers ( this . config . configPath ) ; const firefoxBrowser = _ . find ( availableBrowsers , { name : 'firefox' } ) ; return ! ! firefoxBrowser && parseInt ( firefoxBrowser . version . split ( '.' ) [ 0 ] , 0 ) >= 58 && await getCertutilCommand ( ) . then ( ( ) => true ) . catch ( ( ) => false ) ; } async startFirefox ( initialServer , proxyPort ? , existingPrefs = { } ) { const initialUrl = initialServer . url ; const browser = await launchBrowser ( initialUrl , { browser : 'firefox' , profile : this . firefoxProfilePath , proxy : proxyPort ? `127.0.0.1:${ proxyPort }` : undefined , prefs : _ . assign ( existingPrefs , proxyPort ? { 'network.proxy.ssl' : '"127.0.0.1"' , 'network.proxy.ssl_port' : proxyPort , 'network.proxy.http' : '"127.0.0.1"' , 'network.proxy.http_port' : proxyPort , 'network.proxy.no_proxies_on' : "\"" + initialServer . host + "\"" , 'network.proxy.http.no_proxies_on' : "\"" + initialServer . host + "\"" , 'network.proxy.allow_hijacking_localhost' : true , } : { } , { 'network.captive-portal-service.enabled' : false , 'browser.chrome.toolbar_tips' : false , "app.update.auto" : false , "browser.startup.homepage_override.mstone" : "\"ignore\"" , "browser.showQuitWarning" : false , "browser.tabs.warnOnClose" : false , "browser.tabs.warnOnCloseOtherTabs" : false , "browser.uitour.enabled" : false , 'browser.usedOnWindows10' : true , "browser.usedOnWindows10.introURL" : "\"\"" , 'datareporting.healthreport.service.firstRun' : false , 'toolkit.telemetry.reportingpolicy.firstRun' : false , 'browser.reader.detectedFirstArticle' : false , "datareporting.policy.dataSubmissionEnabled" : false , "datareporting.policy.dataSubmissionPolicyAccepted" : false , "datareporting.policy.dataSubmissionPolicyBypassNotification" : true , "trailhead.firstrun.didSeeAboutWelcome" : true , "privacy.history.custom" : true , "privacy.sanitize.sanitizeOnShutdown" : true , "privacy.clearOnShutdown.cache" : true , "privacy.clearOnShutdown.cookies" : true , "privacy.clearOnShutdown.downloads" : true , "privacy.clearOnShutdown.formdata" : true , "privacy.clearOnShutdown.history" : true , "privacy.clearOnShutdown.offlineApps" : true , "privacy.clearOnShutdown.sessions" : true , "privacy.clearOnShutdown.siteSettings" : true , "privacy.clearOnShutdown.openWindows" : false } ) } , this . config . configPath ) ; console . log ( 'Firefox started' ) ; if ( browser . process . stdout ) browser . process . stdout . pipe ( process . stdout ) ; if ( browser . process . stderr ) browser . process . stderr . pipe ( process . stderr ) ; const normalStop = browser . stop . bind ( browser ) ; browser . stop = async function ( ) { if ( process . platform === "win32" ) { await windowsKill ( `CommandLine Like '%\\\\firefox.exe%${ initialUrl }'` ) . catch ( console . log ) ; } else { normalStop ( ) ; } } ; return browser ; } async setupFirefoxProfile ( ) { const messageServer = new MessageServer ( this . config , `HTTP Toolkit is preparing a Firefox profile, please wait...` ) ; await messageServer . start ( ) ; let messageShown = messageServer . waitForSuccess ( ) . catch ( reportError ) ; profileSetupBrowser = await this . startFirefox ( messageServer ) ; profileSetupBrowser . process . once ( 'close' , ( exitCode ) => { console . log ( "Profile setup Firefox closed" ) ; messageServer . stop ( ) ; profileSetupBrowser = undefined ; if ( messageShown !== true ) { reportError ( `Firefox profile setup failed with code ${ exitCode }` ) ; deleteFolder ( this . firefoxProfilePath ) . catch ( console . warn ) ; } } ) ; await messageShown ; messageShown = true ; await delay ( 200 ) ; profileSetupBrowser . stop ( ) ; await new Promise < void > ( ( resolve ) => { if ( ! profileSetupBrowser ) return resolve ( ) ; else profileSetupBrowser . process . once ( 'close' , resolve ) ; } ) ; const certutil = await getCertutilCommand ( ) ; const certUtilResult = await spawnToResult ( certutil . command , [ '-A' , '-d' , `sql:${ this . firefoxProfilePath }` , '-t' , 'C,,' , '-i' , this . config . https . certPath , '-n' , 'HTTP Toolkit' ] , certutil . options || { } ) ; if ( certUtilResult . exitCode !== 0 ) { console . log ( certUtilResult . stdout ) ; console . log ( certUtilResult . stderr ) ; throw new Error ( `Certutil firefox profile setup failed with code ${ certUtilResult . exitCode }` ) ; } } async activate ( proxyPort ) { if ( this . isActive ( proxyPort ) || ! ! profileSetupBrowser ) return ; const firefoxPrefsFile = path . join ( this . firefoxProfilePath , 'prefs.js' ) ; let existingPrefs = { } ; if ( await canAccess ( firefoxPrefsFile ) === false ) { await this . setupFirefoxProfile ( ) ; } const prefContents = await readFile ( firefoxPrefsFile , { encoding : 'utf8' } ) . catch ( ( ) => '' ) ; existingPrefs = _ ( prefContents ) . split ( '\n' ) . reduce ( ( prefs , line ) => { const match = FIREFOX_PREF_REGEX . exec ( line ) ; if ( match ) { prefs [ match [ 1 ] ] = match [ 2 ] ; } return prefs ; } , { } ) ; const certCheckServer = new CertCheckServer ( this . config ) ; await certCheckServer . start ( "https://amiusing.httptoolkit.tech" ) ; const browser = await this . startFirefox ( certCheckServer , proxyPort , existingPrefs ) ; let certCheckSuccessful ; certCheckServer . waitForSuccess ( ) . then ( ( ) => { certCheckSuccessful = true ; } ) . catch ( ( e ) => { certCheckSuccessful = false ; reportError ( e ) ; } ) ; browsers [ proxyPort ] = browser ; browser . process . once ( 'close' , async ( exitCode ) => { console . log ( 'Firefox closed' ) ; delete browsers [ proxyPort ] ; await delay ( 2000 ) ; certCheckServer . stop ( ) ; if ( ! certCheckSuccessful ) { reportError ( `Firefox certificate check ${ certCheckSuccessful === false ? "failed" : "did not complete" } with FF exit code ${ exitCode }` ) ; deleteFolder ( this . firefoxProfilePath ) . catch ( console . warn ) ; } } ) ; await certCheckServer . waitForSuccess ( ) ; } async deactivate ( proxyPort ) { if ( this . isActive ( proxyPort ) ) { const browser = browsers [ proxyPort ] ; const closePromise = new Promise ( ( resolve ) => browser . process . once ( 'close' , resolve ) ) ; browser . stop ( ) ; await closePromise ; } } async deactivateAll ( ) { await Promise . all ( Object . keys ( browsers ) . map ( ( proxyPort ) => this . deactivate ( proxyPort ) ) ) ; if ( profileSetupBrowser ) { profileSetupBrowser . stop ( ) ; return new Promise ( ( resolve ) => profileSetupBrowser ! . process . once ( 'close' , resolve ) ) ; } } } ;