import * as stream from 'stream' ; import * as path from 'path' ; import * as adb from '@devicefarmer/adbkit' ; import { reportError } from '../../error-tracking' ; import { isErrorLike } from '../../util/error' ; import { delay , waitUntil } from '../../util/promise' ; import { getCertificateFingerprint , parseCert } from '../../certificates' ; export const ANDROID_TEMP = '/data/local/tmp' ; export const SYSTEM_CA_PATH = '/system/etc/security/cacerts' ; export function createAdbClient ( ) { const client = adb . createClient ( { port : process . env [ 'ANDROID_ADB_SERVER_PORT' ] ? parseInt ( process . env [ 'ANDROID_ADB_SERVER_PORT' ] , 10 ) : 5037 , bin : process . env [ 'ANDROID_HOME' ] ? path . join ( process . env [ 'ANDROID_HOME' ] , 'platform-tools' , 'adb' ) : 'adb' } ) ; client . on ( 'error' , reportError ) ; return client ; } const batchCalls = < A extends any [ ] , R > ( fn ) => { let ongoingCall = undefined ; return ( ... args ) => { if ( ! ongoingCall ) { ongoingCall = fn ( ... args ) . then ( ( result ) => { ongoingCall = undefined ; return result ; } ) . catch ( ( error ) => { ongoingCall = undefined ; throw error ; } ) ; } return ongoingCall ; } ; } ; export const getConnectedDevices = batchCalls ( async ( adbClient ) => { try { const devices = await adbClient . listDevices ( ) ; return devices . filter ( d => d . type !== 'offline' && d . type !== 'unauthorized' && ! d . type . startsWith ( "no permissions" ) ) . map ( d => d . id ) ; } catch ( e ) { if ( isErrorLike ( e ) && ( e . code === 'ENOENT' || e . code === 'EACCES' || e . code === 'ECONNREFUSED' || e . code === 'ENOTDIR' || e . signal === 'SIGKILL' || ( e . cmd && e . code ) ) ) { if ( e . code !== 'ENOENT' ) { console . log ( `ADB unavailable, ${ e . cmd ? `${ e . cmd } exited with ${ e . code }` : `due to ${ e . code }` }` ) ; } return [ ] ; } else { reportError ( e ) ; throw e ; } } } ) ; export function stringAsStream ( input ) { const contentStream = new stream . Readable ( ) ; contentStream . _read = ( ) => { } ; contentStream . push ( input ) ; contentStream . push ( null ) ; return contentStream ; } async function run ( adbClient , deviceId , command , options = { timeout : 10000 } ) { return Promise . race ( [ adbClient . shell ( deviceId , command ) . then ( adb . util . readAll ) . then ( buffer => buffer . toString ( 'utf8' ) ) , ... ( options . timeout ? [ delay ( options . timeout ) . then ( ( ) => { throw new Error ( `Timeout for ADB command ${ command }` ) ; } ) ] : [ ] ) ] ) ; } export async function pushFile ( adbClient , deviceId , contents , path , mode ? ) { const transfer = await adbClient . push ( deviceId , contents , path , mode ) ; return new Promise ( ( resolve , reject ) => { transfer . on ( 'end' , resolve ) ; transfer . on ( 'error' , reject ) ; } ) ; } const runAsRootCommands = [ [ 'su' , 'root' ] , [ 'su' , '-c' ] ] ; export async function getRootCommand ( adbClient , deviceId ) { const rootCheckResults = await Promise . all ( runAsRootCommands . map ( ( cmd ) => run ( adbClient , deviceId , cmd . concat ( 'whoami' ) , { timeout : 1000 } ) . catch ( console . log ) . then ( ( whoami ) => ( { cmd , whoami } ) ) ) ) ; const validRootCommands = rootCheckResults . filter ( ( result ) => ( result . whoami || '' ) . trim ( ) === 'root' ) . map ( ( result ) => result . cmd ) ; if ( validRootCommands . length >= 1 ) return validRootCommands [ 0 ] ; await adbClient . root ( deviceId ) . catch ( ( e ) => { if ( e . message && e . message . includes ( "adbd is already running as root" ) ) return ; else console . log ( e ) ; } ) ; await delay ( 500 ) ; const whoami = await waitUntil ( 250 , 10 , ( ) : Promise < string | false > => { return run ( adbClient , deviceId , [ 'whoami' ] ) . catch ( ( ) => false ) ; } ) . catch ( console . log ) ; return ( whoami || '' ) . trim ( ) === 'root' ? [ ] : undefined ; } export async function hasCertInstalled ( adbClient , deviceId , certHash , certFingerprint ) { try { const certPath = `/system/etc/security/cacerts/${ certHash }.0` ; const certStream = await adbClient . pull ( deviceId , certPath ) ; const data = await new Promise < Buffer > ( ( resolve , reject ) => { const data = [ ] ; certStream . on ( 'data' , ( d ) => data . push ( d ) ) ; certStream . on ( 'end' , ( ) => resolve ( Buffer . concat ( data ) ) ) ; certStream . on ( 'error' , reject ) ; } ) ; const existingCert = parseCert ( data . toString ( 'utf8' ) ) ; const existingFingerprint = getCertificateFingerprint ( existingCert ) ; return certFingerprint === existingFingerprint ; } catch ( e ) { return false ; } } export async function injectSystemCertificate ( adbClient , deviceId , rootCmd , certificatePath ) { const injectionScriptPath = `${ ANDROID_TEMP }/htk-inject-system-cert.sh` ; await pushFile ( adbClient , deviceId , stringAsStream ( `
            set -e # Fail on error

            # Create a separate temp directory, to hold the current certificates
            # Without this, when we add the mount we can't read the current certs anymore.
            mkdir -m 700 /data/local/tmp/htk-ca-copy

            # Copy out the existing certificates
            cp /system/etc/security/cacerts/* /data/local/tmp/htk-ca-copy/

            # Create the in-memory mount on top of the system certs folder
            mount -t tmpfs tmpfs /system/etc/security/cacerts

            # Copy the existing certs back into the tmpfs mount, so we keep trusting them
            mv /data/local/tmp/htk-ca-copy/* /system/etc/security/cacerts/

            # Copy our new cert in, so we trust that too
            mv ${ certificatePath } /system/etc/security/cacerts/

            # Update the perms & selinux context labels, so everything is as readable as before
            chown root:root /system/etc/security/cacerts/*
            chmod 644 /system/etc/security/cacerts/*
            chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*

            # Delete the temp cert directory & this script itself
            rm -r /data/local/tmp/htk-ca-copy
            rm ${ injectionScriptPath }

            echo "System cert successfully injected"
        ` ) , injectionScriptPath , 0o444 ) ; const scriptOutput = await run ( adbClient , deviceId , rootCmd . concat ( 'sh' , injectionScriptPath ) ) ; console . log ( scriptOutput ) ; } export async function bringToFront ( adbClient , deviceId , activityName ) { await adbClient . shell ( deviceId , [ "input" , "keyevent" , "KEYCODE_WAKEUP" ] ) ; await delay ( 10 ) ; await adbClient . shell ( deviceId , [ "am" , "start" , "--activity-single-top" , activityName ] ) ; }