format import { IncomingMessage } from 'http' ; import ServerAdapter from './ServerAdapter' ; import WebSocket , { AddressInfo , Server as WSServer , VerifyClientCallbackSync , } from 'ws' ; import { createServer as createHttpsServer } from 'https' ; import { createServer as createHttpServer } from 'http' ; import querystring from 'querystring' ; import { ClientQuery } from 'flipper-common' ; import { assertNotNull , parseClientQuery , parseMessageToJson , verifyClientQueryComesFromCertExchangeSupportedOS , } from './Utilities' ; import { SecureServerConfig } from '../utils/CertificateProvider' ; import { Server } from 'net' ; import { serializeError } from 'serialize-error' ; import { WSCloseCode } from '../utils/WSCloseCode' ; export interface ConnectionCtx { clientQuery ? : ClientQuery ; ws : WebSocket ; request : IncomingMessage ; } export const WEBSOCKET_MAX_MESSAGE_SIZE = 100 * 1024 * 1024 ; /**
 * It serves as a base class for WebSocket based servers. It delegates the 'connection'
 * event to subclasses as a customisation point.
 */ class ServerWebSocket extends ServerAdapter { protected wsServer ? : WSServer ; private httpServer ? : Server ; async start ( port , sslConfig ? ) { const assignedPort = await new Promise < number > ( ( resolve , reject ) => { const server = sslConfig ? createHttpsServer ( sslConfig ) : createHttpServer ( ) ; const wsServer = new WSServer ( { server , verifyClient : this . verifyClient ( ) , maxPayload : WEBSOCKET_MAX_MESSAGE_SIZE , } ) ; const onConnectionError = ( error ) => { reject ( new Error ( `Unable to start server at port ${ port } due to ${ JSON . stringify ( serializeError ( error ) ) }` ) ) ; } ; wsServer . once ( 'error' , onConnectionError ) ; server . listen ( port , ( ) => { console . debug ( `${ sslConfig ? 'Secure' : 'Insecure' } server started on port ${ port }` , 'server' ) ; wsServer . off ( 'error' , onConnectionError ) ; this . listener . onListening ( port ) ; this . wsServer = wsServer ; this . httpServer = server ; resolve ( ( server . address ( ) as AddressInfo ) . port ) ; } ) ; } ) ; assertNotNull ( this . wsServer ) ; assertNotNull ( this . httpServer ) ; this . wsServer . on ( 'connection' , ( ws , request ) => { ws . on ( 'error' , ( error ) => { console . error ( '[conn] WS connection error:' , error ) ; this . listener . onError ( error ) ; } ) ; try { this . onConnection ( ws , request ) ; } catch ( error ) { ws . emit ( 'error' , error ) ; ws . close ( WSCloseCode . InternalError ) ; } } ) ; this . wsServer . on ( 'error' , ( error ) => { console . error ( '[conn] WS server error:' , error ) ; this . listener . onError ( error ) ; } ) ; return assignedPort ; } async stop ( ) { if ( ! this . wsServer ) { return ; } await new Promise < void > ( ( resolve , reject ) => { console . info ( '[conn] Stopping WS server' ) ; assertNotNull ( this . wsServer ) ; this . wsServer . close ( ( err ) => { if ( err ) { reject ( err ) ; return ; } resolve ( ) ; } ) ; } ) ; await new Promise < void > ( ( resolve , reject ) => { console . info ( '[conn] Stopping HTTP server' ) ; assertNotNull ( this . httpServer ) ; this . httpServer . close ( ( err ) => { if ( err ) { reject ( err ) ; return ; } resolve ( ) ; } ) ; } ) ; } param ws param request onConnection ( ws , request ) { const ctx = { ws , request } ; this . handleClientQuery ( ctx ) ; this . handleConnectionAttempt ( ctx ) ; ws . on ( 'message' , async ( message ) => { try { const parsedMessage = this . handleMessageDeserialization ( message ) ; this . handleMessage ( ctx , parsedMessage , message as string ) ; } catch ( error ) { ws . emit ( 'error' , error ) ; ws . close ( WSCloseCode . InternalError ) ; } } ) ; } protected handleClientQuery ( ctx ) { const { request } = ctx ; const query = querystring . decode ( request . url ! . split ( '?' ) [ 1 ] ) ; const clientQuery = this . parseClientQuery ( query ) ; if ( ! clientQuery ) { console . error ( '[conn] Unable to extract the client query from the request URL.' ) ; throw new Error ( '[conn] Unable to extract the client query from the request URL.' ) ; } ctx . clientQuery = clientQuery ; } protected handleConnectionAttempt ( ctx ) { const { clientQuery } = ctx ; assertNotNull ( clientQuery ) ; console . info ( `[conn] Insecure websocket connection attempt: ${ clientQuery . app } on ${ clientQuery . device_id }.` ) ; this . listener . onConnectionAttempt ( clientQuery ) ; } protected handleMessageDeserialization ( message ) { const parsedMessage = parseMessageToJson ( message ) ; if ( ! parsedMessage ) { console . error ( '[conn] Failed to parse message' , message ) ; throw new Error ( `[conn] Failed to parse message` ) ; } return parsedMessage ; } protected async handleMessage ( ctx , parsedMessage , _rawMessage ) { const { clientQuery , ws } = ctx ; assertNotNull ( clientQuery ) ; const response = await this . _onHandleUntrustedMessage ( clientQuery , parsedMessage ) ; if ( response ) { ws . send ( response ) ; } } param message protected parseClientQuery ( query ) { return verifyClientQueryComesFromCertExchangeSupportedOS ( parseClientQuery ( query ) ) ; } returns protected verifyClient ( ) { return ( _info ) => { return true ; } ; } } export default ServerWebSocket ;