import * as _ from 'lodash' ; import { generateSPKIFingerprint } from 'mockttp' ; import { HtkConfig } from '../config' ; import { getAvailableBrowsers , launchBrowser , BrowserInstance , Browser , LaunchOptions } from '../browsers' ; import { delay } from '../util/promise' ; import { readFile , deleteFolder } from '../util/fs' ; import { listRunningProcesses , windowsClose , waitForExit } from '../util/process-management' ; import { HideWarningServer } from '../hide-warning-server' ; import { Interceptor } from '.' ; import { reportError } from '../error-tracking' ; const getBrowserDetails = async ( config , variant ) : Promise < Browser | undefined > => { const browsers = await getAvailableBrowsers ( config . configPath ) ; return _ . find ( browsers , b => b . name === variant ) ; } ; const getChromiumLaunchOptions = async ( browser , config , proxyPort , hideWarningServer ) : Promise < LaunchOptions & { options : Required < LaunchOptions > [ 'options' ] ; } > => { const certificatePem = await readFile ( config . https . certPath , 'utf8' ) ; const spkiFingerprint = generateSPKIFingerprint ( certificatePem ) ; return { browser , proxy : `https://127.0.0.1:${ proxyPort }` , noProxy : [ '<-loopback>' , hideWarningServer . host ] , options : [ `--ignore-certificate-errors-spki-list=${ spkiFingerprint }` , '--disable-background-networking' ] } ; } ; abstract class FreshChromiumBasedInterceptor implements Interceptor { readonly abstract id : string ; readonly abstract version : string ; private readonly activeBrowsers : _ . Dictionary < BrowserInstance > = { } ; constructor ( private config , private variantName ) { } isActive ( proxyPort ) { const browser = this . activeBrowsers [ proxyPort ] ; return ! ! browser && ! ! browser . pid ; } async isActivable ( ) { const browserDetails = await getBrowserDetails ( this . config , this . variantName ) ; return ! ! browserDetails ; } async activate ( proxyPort ) { if ( this . isActive ( proxyPort ) ) return ; const hideWarningServer = new HideWarningServer ( this . config ) ; await hideWarningServer . start ( 'https://amiusing.httptoolkit.tech' ) ; const browserDetails = await getBrowserDetails ( this . config , this . variantName ) ; const browser = await launchBrowser ( hideWarningServer . hideWarningUrl , await getChromiumLaunchOptions ( browserDetails ? browserDetails . name : this . variantName , this . config , proxyPort , hideWarningServer ) , this . config . configPath ) ; if ( browser . process . stdout ) browser . process . stdout . pipe ( process . stdout ) ; if ( browser . process . stderr ) browser . process . stderr . pipe ( process . stderr ) ; await hideWarningServer . completedPromise ; await hideWarningServer . stop ( ) ; this . activeBrowsers [ proxyPort ] = browser ; browser . process . once ( 'close' , async ( ) => { delete this . activeBrowsers [ proxyPort ] ; if ( process . platform === 'win32' && this . variantName === 'opera' ) return ; await delay ( 1000 ) ; if ( Object . keys ( this . activeBrowsers ) . length === 0 && browserDetails && _ . isString ( browserDetails . profile ) ) { const profilePath = browserDetails . profile ; if ( ! profilePath . startsWith ( this . config . configPath ) ) { reportError ( `Unexpected ${ this . variantName } profile location, not deleting: ${ profilePath }` ) ; } else { const profileFolder = browserDetails . profile ; deleteFolder ( profileFolder ) . catch ( async ( ) => { await delay ( 1000 ) ; await deleteFolder ( profileFolder ) ; } ) . catch ( console . warn ) ; } } } ) ; await delay ( 500 ) ; } async deactivate ( proxyPort ) { if ( this . isActive ( proxyPort ) ) { const browser = this . activeBrowsers [ proxyPort ] ; const exitPromise = new Promise ( ( resolve ) => browser ! . process . once ( 'close' , resolve ) ) ; browser ! . stop ( ) ; await exitPromise ; } } async deactivateAll ( ) { await Promise . all ( Object . keys ( this . activeBrowsers ) . map ( ( proxyPort ) => this . deactivate ( proxyPort ) ) ) ; } } ; abstract class ExistingChromiumBasedInterceptor implements Interceptor { readonly abstract id : string ; readonly abstract version : string ; private activeBrowser : { proxyPort : number ; browser : BrowserInstance ; } | undefined ; constructor ( private config , private variantName ) { } async browserDetails ( ) { return getBrowserDetails ( this . config , this . variantName ) ; } isActive ( proxyPort ) { const activeBrowser = this . activeBrowser ; return ! ! activeBrowser && activeBrowser . proxyPort === proxyPort && ! ! activeBrowser . browser . pid ; } async isActivable ( ) { if ( this . activeBrowser ) return false ; return ! ! await this . browserDetails ( ) ; } async findExistingPid ( ) { const processes = await listRunningProcesses ( ) ; const browserDetails = await this . browserDetails ( ) ; if ( ! browserDetails ) { throw new Error ( "Can't intercept existing browser without browser details" ) ; } const browserProcesses = processes . filter ( ( proc ) => { if ( process . platform === 'darwin' ) { if ( ! proc . command . startsWith ( browserDetails . command ) ) return false ; const appBundlePath = proc . command . substring ( browserDetails . command . length ) ; return appBundlePath . match ( /^\/Contents\/MacOS\// ) ; } else { return proc . bin && ( proc . bin === browserDetails . command || proc . bin . includes ( `${ browserDetails . name }/${ browserDetails . type }` ) ) ; } } ) ; const defaultRootProcess = browserProcesses . find ( ( { args } ) => args !== undefined && ! args . includes ( '--type=' ) && ! args . includes ( '--user-data-dir' ) ) ; return defaultRootProcess && defaultRootProcess . pid ; } async activate ( proxyPort , options = { closeConfirmed : false } ) { if ( ! this . isActivable ( ) ) return ; const hideWarningServer = new HideWarningServer ( this . config ) ; await hideWarningServer . start ( 'https://amiusing.httptoolkit.tech' ) ; const existingPid = await this . findExistingPid ( ) ; if ( existingPid ) { if ( ! options . closeConfirmed ) { throw Object . assign ( new Error ( `Not killing ${ this . variantName }: not confirmed` ) , { metadata : { closeConfirmRequired : true } , reportable : false } ) ; } if ( process . platform === 'win32' ) { windowsClose ( existingPid ) ; try { await waitForExit ( existingPid ) ; } catch ( e ) { process . kill ( existingPid ) ; await waitForExit ( existingPid ) ; } } else { process . kill ( existingPid ) ; await waitForExit ( existingPid ) ; } } const browserDetails = await getBrowserDetails ( this . config , this . variantName ) ; const launchOptions = await getChromiumLaunchOptions ( browserDetails ? browserDetails . name : this . variantName , this . config , proxyPort , hideWarningServer ) ; launchOptions . skipDefaults = true ; launchOptions . options . push ( '--no-default-browser-check' , '--no-first-run' , '--disable-popup-blocking' , ... ( existingPid ? [ '--restore-last-session' ] : [ ] ) , hideWarningServer . hideWarningUrl ) ; const browser = await launchBrowser ( "" , { ... launchOptions , skipDefaults : true , profile : null } , this . config . configPath ) ; if ( browser . process . stdout ) browser . process . stdout . pipe ( process . stdout ) ; if ( browser . process . stderr ) browser . process . stderr . pipe ( process . stderr ) ; await hideWarningServer . completedPromise ; await hideWarningServer . stop ( ) ; this . activeBrowser = { browser , proxyPort } ; browser . process . once ( 'close' , async ( ) => { delete this . activeBrowser ; } ) ; await delay ( 500 ) ; } async deactivate ( proxyPort ) { if ( this . isActive ( proxyPort ) ) { const { browser } = this . activeBrowser ! ; if ( process . platform === 'win32' ) { try { await windowsClose ( browser ! . pid ) . then ( ( ) => waitForExit ( browser ! . pid ) ) ; return ; } catch ( e ) { } } const exitPromise = new Promise ( ( resolve ) => browser ! . process . once ( 'close' , resolve ) ) ; browser ! . stop ( ) ; await exitPromise ; } } async deactivateAll ( ) { if ( this . activeBrowser ) { await this . deactivate ( this . activeBrowser . proxyPort ) ; } } } ; export class FreshChrome extends FreshChromiumBasedInterceptor { id = 'fresh-chrome' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chrome' ) ; } } ; export class ExistingChrome extends ExistingChromiumBasedInterceptor { id = 'existing-chrome' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chrome' ) ; } } ; export class FreshChromeBeta extends FreshChromiumBasedInterceptor { id = 'fresh-chrome-beta' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chrome-beta' ) ; } } ; export class FreshChromeDev extends FreshChromiumBasedInterceptor { id = 'fresh-chrome-dev' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chrome-dev' ) ; } } ; export class FreshChromeCanary extends FreshChromiumBasedInterceptor { id = 'fresh-chrome-canary' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chrome-canary' ) ; } } ; export class FreshChromium extends FreshChromiumBasedInterceptor { id = 'fresh-chromium' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chromium' ) ; } } ; export class FreshChromiumDev extends FreshChromiumBasedInterceptor { id = 'fresh-chromium-dev' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'chromium-dev' ) ; } } ; export class FreshEdge extends FreshChromiumBasedInterceptor { id = 'fresh-edge' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'msedge' ) ; } } ; export class FreshEdgeBeta extends FreshChromiumBasedInterceptor { id = 'fresh-edge-beta' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'msedge-beta' ) ; } } ; export class FreshEdgeDev extends FreshChromiumBasedInterceptor { id = 'fresh-edge-dev' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'msedge-dev' ) ; } } ; export class FreshEdgeCanary extends FreshChromiumBasedInterceptor { id = 'fresh-edge-canary' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'msedge-canary' ) ; } } ; export class FreshBrave extends FreshChromiumBasedInterceptor { id = 'fresh-brave' ; version = '1.0.0' ; constructor ( config ) { super ( config , 'brave' ) ; } } ; export class FreshOpera extends FreshChromiumBasedInterceptor { id = 'fresh-opera' ; version = '1.0.3' ; constructor ( config ) { super ( config , 'opera' ) ; } } ;