2021-01-31 05:44:08 -05:00
/ * *
* @ description MeshCentral Firebase communication module
* @ author Ylian Saint - Hilaire
* @ 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" ;
2024-12-15 00:56:36 -05:00
// Initialize the Firebase Admin SDK
module . exports . CreateFirebase = function ( parent , serviceAccount ) {
// Import the Firebase Admin SDK
const admin = require ( 'firebase-admin' ) ;
const 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 = {
2024-12-15 00:56:36 -05:00
mode : 'Real' ,
2021-02-03 02:31:44 -05:00
sent : 0 ,
sendError : 0 ,
received : 0 ,
receivedNoRoute : 0 ,
receivedBadArgs : 0
2024-12-15 00:56:36 -05:00
} ;
const tokenToNodeMap = { } ; // Token --> { nid: nodeid, mid: meshid }
// Initialize Firebase Admin with server key and project ID
if ( ! admin . apps . length ) {
admin . initializeApp ( { credential : admin . credential . cert ( serviceAccount ) } ) ;
2021-02-03 02:31:44 -05:00
}
2024-11-26 13:37:27 -05:00
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 ( ) { }
}
2024-12-15 00:56:36 -05:00
// Function to send notifications
2021-01-31 07:31:32 -05:00
obj . sendToDevice = function ( node , payload , options , func ) {
2024-12-15 00:56:36 -05:00
if ( typeof node === 'string' ) {
parent . db . Get ( node , function ( err , docs ) {
if ( ! err && docs && docs . length === 1 ) {
obj . sendToDeviceEx ( docs [ 0 ] , payload , options , func ) ;
} else {
func ( 0 , 'error' ) ;
}
} ) ;
2021-04-14 02:23:09 -04:00
} else {
obj . sendToDeviceEx ( node , payload , options , func ) ;
}
2024-12-15 00:56:36 -05:00
} ;
2021-04-14 02:23:09 -04:00
// Send an outbound push notification
obj . sendToDeviceEx = function ( node , payload , options , func ) {
2024-12-15 00:56:36 -05:00
if ( ! node || 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 ) ) ;
2024-12-15 00:56:36 -05:00
2021-01-31 07:31:32 -05:00
// Fill in our lookup table
2024-12-15 00:56:36 -05:00
if ( node . _id ) {
tokenToNodeMap [ node . pmt ] = {
nid : node . _id ,
mid : node . meshid ,
did : node . domain
} ;
2021-01-31 05:44:08 -05:00
}
2024-12-15 00:56:36 -05:00
const message = {
token : node . pmt ,
notification : payload . notification ,
data : payload . data ,
android : {
priority : options . priority || 'high' ,
ttl : options . timeToLive ? options . timeToLive * 1000 : undefined
}
} ;
admin . messaging ( ) . send ( message ) . then ( function ( response ) {
obj . stats . sent ++ ;
obj . log ( 'Success' ) ;
func ( response ) ;
} ) . catch ( function ( error ) {
obj . stats . sendError ++ ;
obj . log ( 'Fail: ' + error ) ;
func ( 0 , error ) ;
} ) ;
} ;
2021-02-04 00:48:54 -05:00
// Setup a two way relay
obj . setupRelay = function ( ws ) {
ws . relayId = getRandomPassword ( ) ;
2024-12-15 00:56:36 -05:00
while ( obj . relays [ ws . relayId ] ) { ws . relayId = getRandomPassword ( ) ; }
2021-02-04 00:48:54 -05:00
obj . relays [ ws . relayId ] = ws ;
ws . on ( 'message' , function ( msg ) {
parent . debug ( 'email' , 'FBWS-Data(' + this . relayId + '): ' + msg ) ;
2024-12-15 00:56:36 -05:00
if ( typeof msg === 'string' ) {
2021-03-01 15:24:10 -05:00
obj . log ( 'Relay: ' + msg ) ;
2024-12-15 00:56:36 -05:00
let data ;
try { data = JSON . parse ( msg ) ; } catch ( ex ) { return ; }
if ( typeof data !== 'object' ) return ;
if ( ! parent . common . validateObjectForMongo ( data , 4096 ) ) return ;
if ( typeof data . pmt !== 'string' || typeof data . payload !== 'object' ) return ;
data . payload . data = data . payload . data || { } ;
data . payload . data . r = ws . relayId ;
obj . sendToDevice ( { pmt : data . pmt } , data . payload , data . options , function ( id , err ) {
if ( ! err ) {
try { ws . send ( JSON . stringify ( { sent : true } ) ) ; } catch ( ex ) { }
2021-02-04 00:48:54 -05:00
} else {
2024-12-15 00:56:36 -05:00
try { ws . send ( JSON . stringify ( { sent : false } ) ) ; } catch ( ex ) { }
2021-02-04 00:48:54 -05:00
}
} ) ;
}
} ) ;
2024-12-15 00:56:36 -05:00
2021-02-04 00:48:54 -05:00
// If error, close the relay
ws . on ( 'error' , function ( err ) {
parent . debug ( 'email' , 'FBWS-Error(' + this . relayId + '): ' + err ) ;
delete obj . relays [ this . relayId ] ;
} ) ;
2024-12-15 00:56:36 -05:00
2021-02-04 00:48:54 -05:00
// Close the relay
ws . on ( 'close' , function ( ) {
parent . debug ( 'email' , 'FBWS-Close(' + this . relayId + ')' ) ;
delete obj . relays [ this . relayId ] ;
} ) ;
2024-12-15 00:56:36 -05:00
} ;
function getRandomPassword ( ) {
return Buffer . from ( parent . crypto . randomBytes ( 9 ) , 'binary' ) . toString ( 'base64' ) . replace ( /\//g , '@' ) ;
2021-02-04 00:48:54 -05:00
}
2024-12-15 00:56:36 -05:00
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' ) ;
2024-12-15 00:56:36 -05:00
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 ( ) { }
}
2024-12-15 00:56:36 -05:00
2021-03-01 15:24:10 -05:00
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 ;
2024-12-15 00:56:36 -05:00
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
} ) ;
}
2024-12-15 00:56:36 -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 ; }
2024-12-15 00:56:36 -05:00
2021-02-04 00:48:54 -05:00
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
}
}
2024-12-15 00:56:36 -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 ) ;
}
}
2024-12-15 00:56:36 -05:00
2021-04-14 02:23:09 -04:00
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 ) ) ;
2024-12-15 00:56:36 -05:00
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 } }
2024-12-15 00:56:36 -05:00
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.
2024-12-15 00:56:36 -05:00
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 ;
2024-12-15 00:56:36 -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 ) ;
}
}
2024-12-15 00:56:36 -05:00
2021-04-14 02:23:09 -04:00
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 ;
2024-12-15 00:56:36 -05:00
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.
2024-12-15 00:56:36 -05:00
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 } ) } ) ;
2024-12-15 00:56:36 -05:00
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
}
2024-12-15 00:56:36 -05:00
2021-01-31 05:44:08 -05:00
return obj ;
} ;