2021-01-31 05:44:08 -05:00
/ * *
* @ description MeshCentral Firebase communication module
* @ author Ylian Saint - Hilaire
2022-01-24 02:21:24 -05:00
* @ copyright Intel Corporation 2018 - 2022
2021-01-31 05:44:08 -05:00
* @ license Apache - 2.0
* @ version v0 . 0.1
* /
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
"use strict" ;
// Construct the Firebase object
module . exports . CreateFirebase = function ( parent , senderid , serverkey ) {
var obj = { } ;
2021-01-31 07:31:32 -05:00
obj . messageId = 0 ;
2021-02-04 00:48:54 -05:00
obj . relays = { } ;
2021-02-03 02:31:44 -05:00
obj . stats = {
mode : "Real" ,
sent : 0 ,
sendError : 0 ,
received : 0 ,
receivedNoRoute : 0 ,
receivedBadArgs : 0
}
2021-01-31 05:44:08 -05:00
2024-11-26 13:37:27 -05:00
// In NodeJS v23, add util.isNullOrUndefined() to make node-xcs work correctly.
// Remove this when node-xcs moves to support NodeJS v23
if ( require ( 'util' ) . isNullOrUndefined == null ) { require ( 'util' ) . isNullOrUndefined = function ( v ) { return v == null ; } }
2021-01-31 05:44:08 -05:00
const Sender = require ( 'node-xcs' ) . Sender ;
const Message = require ( 'node-xcs' ) . Message ;
const Notification = require ( 'node-xcs' ) . Notification ;
const xcs = new Sender ( senderid , serverkey ) ;
2021-01-31 07:31:32 -05:00
var tokenToNodeMap = { } // Token --> { nid: nodeid, mid: meshid }
2021-03-01 15:24:10 -05:00
// Setup logging
if ( parent . config . firebase && ( parent . config . firebase . log === true ) ) {
obj . logpath = parent . path . join ( parent . datapath , 'firebase.txt' ) ;
obj . log = function ( msg ) { try { parent . fs . appendFileSync ( obj . logpath , new Date ( ) . toLocaleString ( ) + ': ' + msg + '\r\n' ) ; } catch ( ex ) { console . log ( 'ERROR: Unable to write to firebase.txt.' ) ; } }
} else {
obj . log = function ( ) { }
}
2021-01-31 05:44:08 -05:00
// Messages received from client (excluding receipts)
xcs . on ( 'message' , function ( messageId , from , data , category ) {
2021-03-01 15:24:10 -05:00
const jsonData = JSON . stringify ( data ) ;
obj . log ( 'Firebase-Message: ' + jsonData ) ;
parent . debug ( 'email' , 'Firebase-Message: ' + jsonData ) ;
2021-01-31 07:31:32 -05:00
2021-02-04 00:48:54 -05:00
if ( typeof data . r == 'string' ) {
// Lookup push relay server
parent . debug ( 'email' , 'Firebase-RelayRoute: ' + data . r ) ;
const wsrelay = obj . relays [ data . r ] ;
if ( wsrelay != null ) {
delete data . r ;
try { wsrelay . send ( JSON . stringify ( { from : from , data : data , category : category } ) ) ; } catch ( ex ) { }
}
2021-02-03 02:31:44 -05:00
} else {
2021-02-04 00:48:54 -05:00
// Lookup node information from the cache
var ninfo = tokenToNodeMap [ from ] ;
if ( ninfo == null ) { obj . stats . receivedNoRoute ++ ; return ; }
if ( ( data != null ) && ( data . con != null ) && ( data . s != null ) ) { // Console command
obj . stats . received ++ ;
parent . webserver . routeAgentCommand ( { action : 'msg' , type : 'console' , value : data . con , sessionid : data . s } , ninfo . did , ninfo . nid , ninfo . mid ) ;
} else {
obj . stats . receivedBadArgs ++ ;
}
2021-01-31 07:31:32 -05:00
}
2021-01-31 05:44:08 -05:00
} ) ;
// Only fired for messages where options.delivery_receipt_requested = true
/ *
xcs . on ( 'receipt' , function ( messageId , from , data , category ) { console . log ( 'Firebase-Receipt' , messageId , from , data , category ) ; } ) ;
xcs . on ( 'connected' , function ( ) { console . log ( 'Connected' ) ; } ) ;
xcs . on ( 'disconnected' , function ( ) { console . log ( 'disconnected' ) ; } ) ;
xcs . on ( 'online' , function ( ) { console . log ( 'online' ) ; } ) ;
xcs . on ( 'error' , function ( e ) { console . log ( 'error' , e ) ; } ) ;
xcs . on ( 'message-error' , function ( e ) { console . log ( 'message-error' , e ) ; } ) ;
* /
xcs . start ( ) ;
2021-03-01 15:24:10 -05:00
obj . log ( 'CreateFirebase-Setup' ) ;
2021-02-03 02:31:44 -05:00
parent . debug ( 'email' , 'CreateFirebase-Setup' ) ;
2021-01-31 07:31:32 -05:00
// EXAMPLE
2021-01-31 05:44:08 -05:00
//var payload = { notification: { title: command.title, body: command.msg }, data: { url: obj.msgurl } };
//var options = { priority: 'High', timeToLive: 5 * 60 }; // TTL: 5 minutes, priority 'Normal' or 'High'
2021-01-31 07:31:32 -05:00
obj . sendToDevice = function ( node , payload , options , func ) {
2021-04-14 02:23:09 -04:00
if ( typeof node == 'string' ) {
parent . db . Get ( node , function ( err , docs ) { if ( ( err == null ) && ( docs != null ) && ( docs . length == 1 ) ) { obj . sendToDeviceEx ( docs [ 0 ] , payload , options , func ) ; } else { func ( 0 , 'error' ) ; } } )
} else {
obj . sendToDeviceEx ( node , payload , options , func ) ;
}
}
// Send an outbound push notification
obj . sendToDeviceEx = function ( node , payload , options , func ) {
2021-02-03 02:31:44 -05:00
parent . debug ( 'email' , 'Firebase-sendToDevice' ) ;
2021-01-31 07:31:32 -05:00
if ( ( node == null ) || ( typeof node . pmt != 'string' ) ) return ;
2021-03-01 15:24:10 -05:00
obj . log ( 'sendToDevice, node:' + node . _id + ', payload: ' + JSON . stringify ( payload ) + ', options: ' + JSON . stringify ( options ) ) ;
2021-01-31 07:31:32 -05:00
// Fill in our lookup table
2021-02-03 02:31:44 -05:00
if ( node . _id != null ) { tokenToNodeMap [ node . pmt ] = { nid : node . _id , mid : node . meshid , did : node . domain } }
2021-01-31 07:31:32 -05:00
2021-01-31 05:44:08 -05:00
// Built the on-screen notification
var notification = null ;
if ( payload . notification ) {
2021-04-17 01:30:54 -04:00
var notification = new Notification ( 'ic_message' )
2021-01-31 05:44:08 -05:00
. title ( payload . notification . title )
. body ( payload . notification . body )
. build ( ) ;
}
// Build the message
var message = new Message ( 'msg_' + ( ++ obj . messageId ) ) ;
if ( options . priority ) { message . priority ( options . priority ) ; }
if ( payload . data ) { for ( var i in payload . data ) { message . addData ( i , payload . data [ i ] ) ; } }
2021-02-22 02:23:15 -05:00
if ( ( payload . data == null ) || ( payload . data . shash == null ) ) { message . addData ( 'shash' , parent . webserver . agentCertificateHashBase64 ) ; } // Add the server agent hash, new Android agents will reject notifications that don't have this.
2021-01-31 05:44:08 -05:00
if ( notification ) { message . notification ( notification ) }
message . build ( ) ;
// Send the message
2021-02-03 02:31:44 -05:00
function callback ( result ) {
2021-03-01 15:24:10 -05:00
if ( result . getError ( ) == null ) { obj . stats . sent ++ ; obj . log ( 'Success' ) ; } else { obj . stats . sendError ++ ; obj . log ( 'Fail' ) ; }
2021-02-03 02:31:44 -05:00
callback . func ( result . getMessageId ( ) , result . getError ( ) , result . getErrorDescription ( ) )
}
2021-01-31 05:44:08 -05:00
callback . func = func ;
2021-02-03 02:31:44 -05:00
parent . debug ( 'email' , 'Firebase-sending' ) ;
2021-01-31 07:31:32 -05:00
xcs . sendNoRetry ( message , node . pmt , callback ) ;
2021-01-31 05:44:08 -05:00
}
2021-02-04 00:48:54 -05:00
// Setup a two way relay
obj . setupRelay = function ( ws ) {
// Select and set a relay identifier
ws . relayId = getRandomPassword ( ) ;
while ( obj . relays [ ws . relayId ] != null ) { ws . relayId = getRandomPassword ( ) ; }
obj . relays [ ws . relayId ] = ws ;
// On message, parse it
ws . on ( 'message' , function ( msg ) {
parent . debug ( 'email' , 'FBWS-Data(' + this . relayId + '): ' + msg ) ;
if ( typeof msg == 'string' ) {
2021-03-01 15:24:10 -05:00
obj . log ( 'Relay: ' + msg ) ;
2021-02-04 00:48:54 -05:00
// Parse the incoming push request
var data = null ;
try { data = JSON . parse ( msg ) } catch ( ex ) { return ; }
if ( typeof data != 'object' ) return ;
2021-03-01 15:24:10 -05:00
if ( parent . common . validateObjectForMongo ( data , 4096 ) == false ) return ; // Perform sanity checking on this object.
2021-02-04 00:48:54 -05:00
if ( typeof data . pmt != 'string' ) return ;
if ( typeof data . payload != 'object' ) return ;
if ( typeof data . payload . notification == 'object' ) {
if ( typeof data . payload . notification . title != 'string' ) return ;
if ( typeof data . payload . notification . body != 'string' ) return ;
}
if ( typeof data . options != 'object' ) return ;
if ( ( data . options . priority != 'Normal' ) && ( data . options . priority != 'High' ) ) return ;
if ( ( typeof data . options . timeToLive != 'number' ) || ( data . options . timeToLive < 1 ) ) return ;
if ( typeof data . payload . data != 'object' ) { data . payload . data = { } ; }
data . payload . data . r = ws . relayId ; // Set the relay id.
// Send the push notification
obj . sendToDevice ( { pmt : data . pmt } , data . payload , data . options , function ( id , err , errdesc ) {
if ( err == null ) {
try { wsrelay . send ( JSON . stringify ( { sent : true } ) ) ; } catch ( ex ) { }
} else {
try { wsrelay . send ( JSON . stringify ( { sent : false } ) ) ; } catch ( ex ) { }
}
} ) ;
}
} ) ;
// If error, close the relay
ws . on ( 'error' , function ( err ) {
parent . debug ( 'email' , 'FBWS-Error(' + this . relayId + '): ' + err ) ;
delete obj . relays [ this . relayId ] ;
} ) ;
// Close the relay
ws . on ( 'close' , function ( ) {
parent . debug ( 'email' , 'FBWS-Close(' + this . relayId + ')' ) ;
delete obj . relays [ this . relayId ] ;
} ) ;
}
function getRandomPassword ( ) { return Buffer . from ( parent . crypto . randomBytes ( 9 ) , 'binary' ) . toString ( 'base64' ) . split ( '/' ) . join ( '@' ) ; }
2021-02-03 02:31:44 -05:00
return obj ;
} ;
// Construct the Firebase object
module . exports . CreateFirebaseRelay = function ( parent , url , key ) {
var obj = { } ;
obj . messageId = 0 ;
obj . stats = {
mode : "Relay" ,
sent : 0 ,
sendError : 0 ,
received : 0 ,
receivedNoRoute : 0 ,
receivedBadArgs : 0
}
2021-02-04 00:48:54 -05:00
const WebSocket = require ( 'ws' ) ;
2021-02-03 02:31:44 -05:00
const https = require ( 'https' ) ;
const querystring = require ( 'querystring' ) ;
const relayUrl = require ( 'url' ) . parse ( url ) ;
parent . debug ( 'email' , 'CreateFirebaseRelay-Setup' ) ;
2021-03-01 15:24:10 -05:00
// Setup logging
if ( parent . config . firebaserelay && ( parent . config . firebaserelay . log === true ) ) {
obj . logpath = parent . path . join ( parent . datapath , 'firebaserelay.txt' ) ;
obj . log = function ( msg ) { try { parent . fs . appendFileSync ( obj . logpath , new Date ( ) . toLocaleString ( ) + ': ' + msg + '\r\n' ) ; } catch ( ex ) { console . log ( 'ERROR: Unable to write to firebaserelay.txt.' ) ; } }
} else {
obj . log = function ( ) { }
}
obj . log ( 'Starting relay to: ' + relayUrl . href ) ;
2021-02-04 00:48:54 -05:00
if ( relayUrl . protocol == 'wss:' ) {
// Setup two-way push notification channel
obj . wsopen = false ;
2021-03-01 15:24:10 -05:00
obj . tokenToNodeMap = { } ; // Token --> { nid: nodeid, mid: meshid }
obj . backoffTimer = 0 ;
2021-02-04 00:48:54 -05:00
obj . connectWebSocket = function ( ) {
2021-03-01 15:24:10 -05:00
if ( obj . reconnectTimer != null ) { try { clearTimeout ( obj . reconnectTimer ) ; } catch ( ex ) { } delete obj . reconnectTimer ; }
2021-02-04 00:48:54 -05:00
if ( obj . wsclient != null ) return ;
obj . wsclient = new WebSocket ( relayUrl . href + ( key ? ( '?key=' + key ) : '' ) , { rejectUnauthorized : false } )
2021-02-04 21:37:38 -05:00
obj . wsclient . on ( 'open' , function ( ) {
2021-03-01 15:24:10 -05:00
obj . lastConnect = Date . now ( ) ;
2021-02-04 21:37:38 -05:00
parent . debug ( 'email' , 'FBWS-Connected' ) ;
obj . wsopen = true ;
} ) ;
2021-02-04 00:48:54 -05:00
obj . wsclient . on ( 'message' , function ( msg ) {
parent . debug ( 'email' , 'FBWS-Data(' + msg . length + '): ' + msg ) ;
2021-03-01 15:24:10 -05:00
obj . log ( 'Received(' + msg . length + '): ' + msg ) ;
2021-02-04 00:48:54 -05:00
var data = null ;
try { data = JSON . parse ( msg ) } catch ( ex ) { }
if ( typeof data != 'object' ) return ;
if ( typeof data . from != 'string' ) return ;
if ( typeof data . data != 'object' ) return ;
if ( typeof data . category != 'string' ) return ;
processMessage ( data . messageId , data . from , data . data , data . category ) ;
} ) ;
2021-03-01 15:24:10 -05:00
obj . wsclient . on ( 'error' , function ( err ) { obj . log ( 'Error: ' + err ) ; } ) ;
obj . wsclient . on ( 'close' , function ( a , b , c ) {
2021-02-04 21:37:38 -05:00
parent . debug ( 'email' , 'FBWS-Disconnected' ) ;
2021-02-04 00:48:54 -05:00
obj . wsclient = null ;
obj . wsopen = false ;
2021-03-01 15:24:10 -05:00
// Compute the backoff timer
if ( obj . reconnectTimer == null ) {
if ( ( obj . lastConnect != null ) && ( ( Date . now ( ) - obj . lastConnect ) > 10000 ) ) { obj . backoffTimer = 0 ; }
obj . backoffTimer += 1000 ;
obj . backoffTimer = obj . backoffTimer * 2 ;
if ( obj . backoffTimer > 1200000 ) { obj . backoffTimer = 600000 ; } // Maximum 10 minutes backoff.
obj . reconnectTimer = setTimeout ( obj . connectWebSocket , obj . backoffTimer ) ;
}
2021-02-04 00:48:54 -05:00
} ) ;
}
2021-02-03 02:31:44 -05:00
2021-02-04 00:48:54 -05:00
function processMessage ( messageId , from , data , category ) {
// Lookup node information from the cache
var ninfo = obj . tokenToNodeMap [ from ] ;
if ( ninfo == null ) { obj . stats . receivedNoRoute ++ ; return ; }
if ( ( data != null ) && ( data . con != null ) && ( data . s != null ) ) { // Console command
obj . stats . received ++ ;
parent . webserver . routeAgentCommand ( { action : 'msg' , type : 'console' , value : data . con , sessionid : data . s } , ninfo . did , ninfo . nid , ninfo . mid ) ;
} else {
obj . stats . receivedBadArgs ++ ;
2021-02-03 02:31:44 -05:00
}
}
2021-02-04 00:48:54 -05:00
obj . sendToDevice = function ( node , payload , options , func ) {
2021-04-14 02:23:09 -04:00
if ( typeof node == 'string' ) {
parent . db . Get ( node , function ( err , docs ) { if ( ( err == null ) && ( docs != null ) && ( docs . length == 1 ) ) { obj . sendToDeviceEx ( docs [ 0 ] , payload , options , func ) ; } else { func ( 0 , 'error' ) ; } } )
} else {
obj . sendToDeviceEx ( node , payload , options , func ) ;
}
}
obj . sendToDeviceEx = function ( node , payload , options , func ) {
2021-02-04 00:48:54 -05:00
parent . debug ( 'email' , 'Firebase-sendToDevice-webSocket' ) ;
if ( ( node == null ) || ( typeof node . pmt != 'string' ) ) { func ( 0 , 'error' ) ; return ; }
2021-03-01 15:24:10 -05:00
obj . log ( 'sendToDevice, node:' + node . _id + ', payload: ' + JSON . stringify ( payload ) + ', options: ' + JSON . stringify ( options ) ) ;
2021-02-04 00:48:54 -05:00
// Fill in our lookup table
if ( node . _id != null ) { obj . tokenToNodeMap [ node . pmt ] = { nid : node . _id , mid : node . meshid , did : node . domain } }
2021-02-22 02:23:15 -05:00
// Fill in the server agent cert hash
if ( payload . data == null ) { payload . data = { } ; }
if ( payload . data . shash == null ) { payload . data . shash = parent . webserver . agentCertificateHashBase64 ; } // Add the server agent hash, new Android agents will reject notifications that don't have this.
2021-02-04 00:48:54 -05:00
// If the web socket is open, send now
if ( obj . wsopen == true ) {
2021-02-04 03:03:51 -05:00
try { obj . wsclient . send ( JSON . stringify ( { pmt : node . pmt , payload : payload , options : options } ) ) ; } catch ( ex ) { func ( 0 , 'error' ) ; obj . stats . sendError ++ ; return ; }
obj . stats . sent ++ ;
2021-03-01 15:24:10 -05:00
obj . log ( 'Sent' ) ;
2021-02-04 00:48:54 -05:00
func ( 1 ) ;
} else {
// TODO: Buffer the push messages until TTL.
2021-02-04 03:03:51 -05:00
obj . stats . sendError ++ ;
2021-03-01 15:24:10 -05:00
obj . log ( 'Error' ) ;
func ( 0 , 'error' ) ;
2021-02-04 00:48:54 -05:00
}
}
obj . connectWebSocket ( ) ;
} else if ( relayUrl . protocol == 'https:' ) {
// Send an outbound push notification using an HTTPS POST
obj . pushOnly = true ;
2021-04-14 02:23:09 -04:00
2021-02-04 00:48:54 -05:00
obj . sendToDevice = function ( node , payload , options , func ) {
2021-04-14 02:23:09 -04:00
if ( typeof node == 'string' ) {
parent . db . Get ( node , function ( err , docs ) { if ( ( err == null ) && ( docs != null ) && ( docs . length == 1 ) ) { obj . sendToDeviceEx ( docs [ 0 ] , payload , options , func ) ; } else { func ( 0 , 'error' ) ; } } )
} else {
obj . sendToDeviceEx ( node , payload , options , func ) ;
}
}
obj . sendToDeviceEx = function ( node , payload , options , func ) {
2021-02-04 00:48:54 -05:00
parent . debug ( 'email' , 'Firebase-sendToDevice-httpPost' ) ;
if ( ( node == null ) || ( typeof node . pmt != 'string' ) ) return ;
2021-02-22 02:23:15 -05:00
// Fill in the server agent cert hash
if ( payload . data == null ) { payload . data = { } ; }
if ( payload . data . shash == null ) { payload . data . shash = parent . webserver . agentCertificateHashBase64 ; } // Add the server agent hash, new Android agents will reject notifications that don't have this.
2021-04-14 18:24:37 -04:00
obj . log ( 'sendToDevice, node:' + node . _id + ', payload: ' + JSON . stringify ( payload ) + ', options: ' + JSON . stringify ( options ) ) ;
const querydata = querystring . stringify ( { 'msg' : JSON . stringify ( { pmt : node . pmt , payload : payload , options : options } ) } ) ;
2021-02-04 00:48:54 -05:00
// Send the message to the relay
const httpOptions = {
hostname : relayUrl . hostname ,
port : relayUrl . port ? relayUrl . port : 443 ,
path : relayUrl . path + ( key ? ( '?key=' + key ) : '' ) ,
method : 'POST' ,
//rejectUnauthorized: false, // DEBUG
headers : {
'Content-Type' : 'application/x-www-form-urlencoded' ,
'Content-Length' : querydata . length
}
}
const req = https . request ( httpOptions , function ( res ) {
2021-03-01 15:24:10 -05:00
obj . log ( 'Response: ' + res . statusCode ) ;
2021-02-04 00:48:54 -05:00
if ( res . statusCode == 200 ) { obj . stats . sent ++ ; } else { obj . stats . sendError ++ ; }
if ( func != null ) { func ( ++ obj . messageId , ( res . statusCode == 200 ) ? null : 'error' ) ; }
} ) ;
parent . debug ( 'email' , 'Firebase-sending' ) ;
req . on ( 'error' , function ( error ) { obj . stats . sent ++ ; func ( ++ obj . messageId , 'error' ) ; } ) ;
req . write ( querydata ) ;
req . end ( ) ;
}
2021-02-03 02:31:44 -05:00
}
2021-01-31 05:44:08 -05:00
return obj ;
} ;