import * as _ from 'lodash' ; import { spawn } from 'child_process' ; import * as path from 'path' ; import { getPortPromise as getPort } from 'portfinder' ; import { generateSPKIFingerprint } from 'mockttp' ; import ChromeRemoteInterface = require ( 'chrome-remote-interface' ) ; import { Interceptor } from '.' ; import { HtkConfig } from '../config' ; import { delay } from '../util/promise' ; import { isErrorLike } from '../util/error' ; import { readFile } from '../util/fs' ; import { windowsClose } from '../util/process-management' ; import { getTerminalEnvVars , OVERRIDES_DIR } from './terminal/terminal-env-overrides' ; import { reportError , addBreadcrumb } from '../error-tracking' ; import { findExecutableInApp } from '@httptoolkit/osx-find-executable' ; const isAppBundle = ( path ) => { return process . platform === "darwin" && path . endsWith ( ".app" ) ; } ; export class ElectronInterceptor implements Interceptor { readonly id = 'electron' ; readonly version = '1.0.1' ; private debugClients : { [ port ] : Array < ChromeRemoteInterface . CdpClient > ; } = { } ; constructor ( private config ) { } private certData = readFile ( this . config . https . certPath , 'utf8' ) ; async isActivable ( ) { return true ; } isActive ( proxyPort ) { return ! ! this . debugClients [ proxyPort ] && ! ! this . debugClients [ proxyPort ] . length ; } async activate ( proxyPort , options ) { const debugPort = await getPort ( { port : proxyPort } ) ; const { pathToApplication } = options ; const cmd = isAppBundle ( pathToApplication ) ? await findExecutableInApp ( pathToApplication ) : pathToApplication ; const appProcess = spawn ( cmd , [ `--inspect-brk=${ debugPort }` ] , { stdio : 'inherit' , env : { ... process . env , ... getTerminalEnvVars ( proxyPort , this . config . https , process . env ) , NODE_OPTIONS : '' } } ) ; let debugClient ; let retries = 10 ; appProcess . on ( 'error' , async ( e ) => { reportError ( e ) ; if ( debugClient ) { try { await debugClient . close ( ) ; } catch ( e ) { } } retries = - 1 ; } ) ; while ( ! debugClient && retries >= 0 ) { try { debugClient = await ChromeRemoteInterface ( { port : debugPort } ) ; } catch ( error ) { if ( ( isErrorLike ( error ) && error . code !== 'ECONNREFUSED' ) || retries === 0 ) { throw error ; } retries = retries - 1 ; await delay ( 500 ) ; } } if ( ! debugClient ) throw new Error ( 'Could not initialize CDP client' ) ; this . debugClients [ proxyPort ] = this . debugClients [ proxyPort ] || [ ] ; this . debugClients [ proxyPort ] . push ( debugClient ) ; debugClient . once ( 'disconnect' , ( ) => { _ . remove ( this . debugClients [ proxyPort ] , c => c === debugClient ) ; } ) ; await debugClient . Runtime . enable ( ) ; await debugClient . Debugger . enable ( ) ; const callFramePromise = new Promise < string > ( ( resolve ) => { debugClient ! . Debugger . paused ( ( stack ) => { resolve ( stack . callFrames [ 0 ] . callFrameId ) ; } ) ; } ) ; await debugClient . Runtime . runIfWaitingForDebugger ( ) ; const callFrameId = await callFramePromise ; console . log ( "Injecting interception settings into Electron app..." ) ; const injectionResult = await debugClient . Debugger . evaluateOnCallFrame ( { expression : `require(${ JSON . stringify ( path . join ( OVERRIDES_DIR , 'js' , 'prepend-electron.js' ) ) })({
                newlineEncodedCertData: "${ ( await this . certData ) . replace ( /\r\n|\r|\n/g , '\\n' ) }",
                spkiFingerprint: "${ generateSPKIFingerprint ( await this . certData ) }"
            })` , callFrameId } ) ; if ( injectionResult . exceptionDetails ) { const exception = injectionResult . exceptionDetails as any ; console . log ( exception ) ; addBreadcrumb ( "Evaluate error" , { message : exception && exception . description , data : injectionResult . exceptionDetails as { [ key ] : any ; } } ) ; throw new Error ( "Failed to inject into Electron app" ) ; } console . log ( "App intercepted, resuming..." ) ; await debugClient . Debugger . resume ( ) ; console . log ( "App resumed, Electron interception complete" ) ; } async deactivate ( proxyPort ) { if ( ! this . isActive ( proxyPort ) ) return ; await Promise . all ( this . debugClients [ proxyPort ] . map ( async ( debugClient ) => { let shutdown = false ; const disconnectPromise = new Promise < void > ( ( resolve ) => debugClient . once ( 'disconnect' , resolve ) ) . then ( ( ) => { shutdown = true ; } ) ; const pidResult = ( await debugClient . Runtime . evaluate ( { expression : 'process.pid' } ) . catch ( ( ) => ( { result : undefined } ) ) ) . result as { type ? : string ; value ? : unknown ; } | undefined ; const pid = pidResult && pidResult . type === 'number' ? pidResult . value as number : undefined ; if ( _ . isNumber ( pid ) ) { if ( process . platform === 'win32' ) { await windowsClose ( pid ) ; } else { process . kill ( pid , "SIGTERM" ) ; } await Promise . race ( [ disconnectPromise , delay ( 1000 ) ] ) ; } if ( ! shutdown ) { await Promise . race ( [ debugClient . Runtime . evaluate ( { expression : 'process.exit(0)' } ) . catch ( ( ) => { } ) , disconnectPromise ] ) ; } ; } ) ) ; } async deactivateAll ( ) { await Promise . all < void > ( Object . keys ( this . debugClients ) . map ( port => this . deactivate ( port ) ) ) ; } }