format import { CertificateExchangeMedium } from '../utils/CertificateProvider' ; import { ClientDescription , ClientQuery , isTest , GK , buildClientId , Logger , UninitializedClient , reportPlatformFailures , } from 'flipper-common' ; import CertificateProvider from '../utils/CertificateProvider' ; import { ClientConnection , ConnectionStatus } from './ClientConnection' ; import { EventEmitter } from 'events' ; import invariant from 'invariant' ; import DummyDevice from '../devices/DummyDevice' ; import { appNameWithUpdateHint , cloneClientQuerySafeForLogging , transformCertificateExchangeMediumToType , } from './Utilities' ; import ServerAdapter , { SecureClientQuery , ServerEventsListener , } from './ServerAdapter' ; import { createBrowserServer , createServer , TransportType , } from './ServerFactory' ; import { FlipperServerImpl } from '../FlipperServerImpl' ; import { getServerPortsConfig , getFlipperServerConfig , } from '../FlipperServerConfig' ; type ClientTimestampTracker = { insecureStart ? : number ; secureStart ? : number ; } ; type ClientInfo = { connection : ClientConnection | null | undefined ; client : ClientDescription ; } ; type ClientCsrQuery = { csr ? : string | undefined ; csr_path ? : string | undefined ; } ; interface ServerController { on ( event , callback ) : this ; } /**
 * Responsible of creating and managing the actual underlying servers:
 * - Insecure (used for certificate exchange)
 * - Secure (used for secure communication between the client and server)
 * - Browser (only ever used between Desktop and a local Browser)
 *
 * Additionally, it manages client connections.
 */ class ServerController extends EventEmitter implements ServerEventsListener { connections : Map < string , ClientInfo > = new Map ( ) ; timestamps : Map < string , ClientTimestampTracker > = new Map ( ) ; secureServer : ServerAdapter | null = null ; insecureServer : ServerAdapter | null = null ; altSecureServer : ServerAdapter | null = null ; altInsecureServer : ServerAdapter | null = null ; browserServer : ServerAdapter | null = null ; certificateProvider : CertificateProvider ; connectionTracker : ConnectionTracker ; flipperServer : FlipperServerImpl ; timeHandlers : Map < string , NodeJS . Timeout > = new Map ( ) ; constructor ( flipperServer ) { super ( ) ; this . flipperServer = flipperServer ; this . certificateProvider = new CertificateProvider ( this , getFlipperServerConfig ( ) . settings ) ; this . connectionTracker = new ConnectionTracker ( this . logger ) ; } onClientMessage ( clientId , payload ) { this . flipperServer . emit ( 'client-message' , { id : clientId , message : payload , } ) ; } get logger ( ) : Logger { return this . flipperServer . logger ; } /**
     * Loads the secure server configuration and starts any necessary servers.
     * Initialisation is complete once the initialized promise is fullfilled at
     * which point Flipper is accepting connections.
     */ async init ( ) { if ( isTest ( ) ) { throw new Error ( 'Spawing new server is not supported in test' ) ; } await this . certificateProvider . init ( ) ; const { insecure , secure } = getServerPortsConfig ( ) . serverPorts ; const options = await this . certificateProvider . loadSecureServerConfig ( ) ; console . info ( '[conn] secure server listening at port: ' , secure ) ; this . secureServer = await createServer ( secure , this , options ) ; const { secure : altSecure } = getServerPortsConfig ( ) . altServerPorts ; console . info ( '[conn] secure server (ws) listening at port: ' , altSecure ) ; this . altSecureServer = await createServer ( altSecure , this , options , TransportType . WebSocket ) ; console . info ( '[conn] insecure server listening at port: ' , insecure ) ; this . insecureServer = await createServer ( insecure , this ) ; const { insecure : altInsecure } = getServerPortsConfig ( ) . altServerPorts ; console . info ( '[conn] insecure server (ws) listening at port: ' , altInsecure ) ; this . altInsecureServer = await createServer ( altInsecure , this , undefined , TransportType . WebSocket ) ; if ( GK . get ( 'comet_enable_flipper_connection' ) ) { console . info ( '[conn] Browser server (ws) listening at port: ' , 8333 ) ; this . browserServer = await createBrowserServer ( 8333 , this ) ; } } /**
     * If initialized, it stops any started servers.
     */ async close ( ) { await Promise . all ( [ this . insecureServer ?. stop ( ) , this . secureServer ?. stop ( ) , this . altInsecureServer ?. stop ( ) , this . altSecureServer ?. stop ( ) , this . browserServer ?. stop ( ) , ] ) ; } onConnectionCreated ( clientQuery , clientConnection , downgrade ) { const { app , os , device , device_id , sdk_version , csr , csr_path , medium } = clientQuery ; const transformedMedium = transformCertificateExchangeMediumToType ( medium ) ; console . info ( `[conn] Connection established: ${ app } on ${ device_id }. Medium ${ medium }. CSR: ${ csr_path }` , cloneClientQuerySafeForLogging ( clientQuery ) ) ; return this . addConnection ( clientConnection , { app , os , device , device_id , sdk_version , medium : transformedMedium , } , { csr , csr_path } , downgrade ) ; } onConnectionClosed ( clientId ) { this . removeConnection ( clientId ) ; } onListening ( port ) { this . emit ( 'listening' , port ) ; } onSecureConnectionAttempt ( clientQuery ) { const strippedClientQuery = ( ( { device_id , ... o } ) => o ) ( clientQuery ) ; let id = buildClientId ( { device_id : 'unknown' , ... strippedClientQuery } ) ; const tracker = this . timestamps . get ( id ) ; if ( tracker ) { this . timestamps . delete ( id ) ; } id = buildClientId ( clientQuery ) ; this . timestamps . set ( id , { secureStart : Date . now ( ) , ... tracker , } ) ; this . logger . track ( 'usage' , 'trusted-request-handler-called' , ( ( { csr , ... o } ) => o ) ( clientQuery ) ) ; const { os , app , device_id } = clientQuery ; if ( os === 'iOS' && ! getFlipperServerConfig ( ) . settings . enableIOS ) { console . error ( `Refusing connection from ${ app } on ${ device_id }, since iOS support is disabled in settings` ) ; return ; } if ( os === 'Android' && ! getFlipperServerConfig ( ) . settings . enableAndroid ) { console . error ( `Refusing connection from ${ app } on ${ device_id }, since Android support is disabled in settings` ) ; return ; } this . connectionTracker . logConnectionAttempt ( clientQuery ) ; if ( this . timeHandlers . get ( clientQueryToKey ( clientQuery ) ) ) { clearTimeout ( this . timeHandlers . get ( clientQueryToKey ( clientQuery ) ) ! ) ; } const transformedMedium = transformCertificateExchangeMediumToType ( clientQuery . medium ) ; if ( transformedMedium === 'WWW' || transformedMedium === 'NONE' ) { this . flipperServer . registerDevice ( new DummyDevice ( this . flipperServer , clientQuery . device_id , clientQuery . app + ( transformedMedium === 'WWW' ? ' Server Exchanged' : '' ) , clientQuery . os ) ) ; } } onConnectionAttempt ( clientQuery ) { const strippedClientQuery = ( ( { device_id , ... o } ) => o ) ( clientQuery ) ; const id = buildClientId ( { device_id : 'unknown' , ... strippedClientQuery } ) ; this . timestamps . set ( id , { insecureStart : Date . now ( ) , } ) ; this . logger . track ( 'usage' , 'untrusted-request-handler-called' , clientQuery ) ; this . connectionTracker . logConnectionAttempt ( clientQuery ) ; const client = { os : clientQuery . os , deviceName : clientQuery . device , appName : appNameWithUpdateHint ( clientQuery ) , } ; this . emit ( 'start-client-setup' , client ) ; } onProcessCSR ( unsanitizedCSR , clientQuery , appDirectory , medium ) { return new Promise ( ( resolve , reject ) => { reportPlatformFailures ( this . certificateProvider . processCertificateSigningRequest ( unsanitizedCSR , clientQuery . os , appDirectory , medium ) , 'processCertificateSigningRequest' ) . then ( ( response ) => { const client = { os : clientQuery . os , deviceName : clientQuery . device , appName : appNameWithUpdateHint ( clientQuery ) , } ; this . timeHandlers . set ( clientQueryToKey ( { ... clientQuery , device_id : response . deviceId } ) , setTimeout ( ( ) => { this . emit ( 'client-unresponsive-error' , { client , medium , deviceID : response . deviceId , } ) ; } , 30 * 1000 ) ) ; resolve ( response ) ; } ) . catch ( ( error ) => { reject ( error ) ; } ) ; } ) ; } onError ( error ) { this . emit ( 'error' , error ) ; } toJSON ( ) { return null ; } param connection param query param csrQuery async addConnection ( connection , query , csrQuery , silentReplace ? ) { invariant ( query , 'expected query' ) ; const { csr_path , csr } = csrQuery ; if ( csr_path && csr && query . os === 'Android' ) { const app_name = await this . certificateProvider . extractAppNameFromCSR ( csr ) ; ( query as any ) . device_id = await this . certificateProvider . getTargetDeviceId ( query . os , app_name , csr_path , csr ) ; console . info ( `[conn] Detected ${ app_name } on ${ query . device_id } in certificate` , query ) ; } ( query as any ) . app = appNameWithUpdateHint ( query ) ; const id = buildClientId ( query ) ; console . info ( `[conn] Matching device for ${ query . app } on ${ query . device_id }...` , query ) ; const client = { id , query , } ; const info = { client , connection : connection , } ; console . info ( `[conn] Initializing client ${ query . app } on ${ query . device_id }...` , query ) ; connection . subscribeToEvents ( ( status ) => { if ( status === ConnectionStatus . CLOSED || status === ConnectionStatus . ERROR ) { this . onConnectionClosed ( client . id ) ; } } ) ; console . debug ( `[conn] Device client initialized: ${ id }.` , 'server' , query ) ; if ( this . connections . has ( id ) ) { const connectionInfo = this . connections . get ( id ) ; if ( connectionInfo ) { if ( connectionInfo . connection && connectionInfo . connection !== connection ) { if ( ! silentReplace ) { connectionInfo . connection . close ( ) ; } this . removeConnection ( id ) ; } } } this . connections . set ( id , info ) ; this . flipperServer . emit ( 'client-connected' , client ) ; const tracker = this . timestamps . get ( id ) ; if ( tracker ) { const end = Date . now ( ) ; const start = tracker . insecureStart ? tracker . insecureStart : tracker . secureStart ; const elapsed = Math . round ( end - start ! ) ; this . logger . track ( 'performance' , 'client-connection-tracker' , { 'time-to-connection' : elapsed , ... query , } ) ; this . timestamps . delete ( id ) ; } return client ; } attachFakeClient ( client ) { this . connections . set ( client . id , { client , connection : null , } ) ; } param id removeConnection ( id ) { const info = this . connections . get ( id ) ; if ( info ) { console . info ( `[conn] Disconnected: ${ info . client . query . app } on ${ info . client . query . device_id }.` , info . client . query ) ; this . flipperServer . emit ( 'client-disconnected' , { id } ) ; this . connections . delete ( id ) ; } } } class ConnectionTracker { timeWindowMillis = 20 * 1000 ; connectionProblemThreshold = 4 ; connectionAttempts : Map < string , Array < number > > = new Map ( ) ; logger : Logger ; constructor ( logger ) { this . logger = logger ; } logConnectionAttempt ( client ) { const key = `${ client . os }-${ client . device }-${ client . app }` ; const time = Date . now ( ) ; let entry = this . connectionAttempts . get ( key ) || [ ] ; entry . push ( time ) ; entry = entry . filter ( ( t ) => t >= time - this . timeWindowMillis ) ; this . connectionAttempts . set ( key , entry ) ; if ( entry . length >= this . connectionProblemThreshold ) { console . warn ( `[conn] Connection loop detected with ${ key }. Connected ${ this . connectionProblemThreshold } times within ${ this . timeWindowMillis / 1000 }s.` , 'server' , client ) ; } } } export default ServerController ; function clientQueryToKey ( clientQuery ) { return `${ clientQuery . app }/${ clientQuery . os }/${ clientQuery . device }/${ clientQuery . device_id }` ; }