Updated to MeshCentral Firebase support, updated to using firebase-admin module.

Ylian Saint-Hilaire committed Dec 14, 2024 at 21:56 UTC c2eb1f25162b2138d445733e22d7ca8d31278ed4
3 files changed +118 -170
firebase.js
+100 -148
@@ -1,7 +1,6 @@
1 /**
2 * @description MeshCentral Firebase communication module
3 * @author Ylian Saint-Hilaire
4 -* @copyright Intel Corporation 2018-2022
4 * @license Apache-2.0
5 * @version v0.0.1
6 */
@@ -14,31 +13,31 @@
13 /*jshint esversion: 6 */
14 "use strict";
15
17 -// Construct the Firebase object
18 -module.exports.CreateFirebase = function (parent, senderid, serverkey) {
19 - var obj = {};
16 +// Initialize the Firebase Admin SDK
17 +module.exports.CreateFirebase = function (parent, serviceAccount) {
18 +
19 + // Import the Firebase Admin SDK
20 + const admin = require('firebase-admin');
21 +
22 + const obj = {};
23 obj.messageId = 0;
24 obj.relays = {};
25 obj.stats = {
23 - mode: "Real",
26 + mode: 'Real',
27 sent: 0,
28 sendError: 0,
29 received: 0,
30 receivedNoRoute: 0,
31 receivedBadArgs: 0
32 + };
33 +
34 + const tokenToNodeMap = {}; // Token --> { nid: nodeid, mid: meshid }
35 +
36 + // Initialize Firebase Admin with server key and project ID
37 + if (!admin.apps.length) {
38 + admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
39 }
30 -
31 - // In NodeJS v23, add util.isNullOrUndefined() to make node-xcs work correctly.
32 - // Remove this when node-xcs moves to support NodeJS v23
33 - if (require('util').isNullOrUndefined == null) { require('util').isNullOrUndefined = function (v) { return v == null; } }
40
35 - const Sender = require('node-xcs').Sender;
36 - const Message = require('node-xcs').Message;
37 - const Notification = require('node-xcs').Notification;
38 - const xcs = new Sender(senderid, serverkey);
39 -
40 - var tokenToNodeMap = {} // Token --> { nid: nodeid, mid: meshid }
41 -
41 // Setup logging
42 if (parent.config.firebase && (parent.config.firebase.log === true)) {
43 obj.logpath = parent.path.join(parent.datapath, 'firebase.txt');
@@ -46,155 +45,108 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) {
45 } else {
46 obj.log = function () { }
47 }
49 -
50 - // Messages received from client (excluding receipts)
51 - xcs.on('message', function (messageId, from, data, category) {
52 - const jsonData = JSON.stringify(data);
53 - obj.log('Firebase-Message: ' + jsonData);
54 - parent.debug('email', 'Firebase-Message: ' + jsonData);
55 -
56 - if (typeof data.r == 'string') {
57 - // Lookup push relay server
58 - parent.debug('email', 'Firebase-RelayRoute: ' + data.r);
59 - const wsrelay = obj.relays[data.r];
60 - if (wsrelay != null) {
61 - delete data.r;
62 - try { wsrelay.send(JSON.stringify({ from: from, data: data, category: category })); } catch (ex) { }
63 - }
64 - } else {
65 - // Lookup node information from the cache
66 - var ninfo = tokenToNodeMap[from];
67 - if (ninfo == null) { obj.stats.receivedNoRoute++; return; }
68 -
69 - if ((data != null) && (data.con != null) && (data.s != null)) { // Console command
70 - obj.stats.received++;
71 - parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid);
72 - } else {
73 - obj.stats.receivedBadArgs++;
74 - }
75 - }
76 - });
77 -
78 - // Only fired for messages where options.delivery_receipt_requested = true
79 - /*
80 - xcs.on('receipt', function (messageId, from, data, category) { console.log('Firebase-Receipt', messageId, from, data, category); });
81 - xcs.on('connected', function () { console.log('Connected'); });
82 - xcs.on('disconnected', function () { console.log('disconnected'); });
83 - xcs.on('online', function () { console.log('online'); });
84 - xcs.on('error', function (e) { console.log('error', e); });
85 - xcs.on('message-error', function (e) { console.log('message-error', e); });
86 - */
87 -
88 - xcs.start();
89 -
90 - obj.log('CreateFirebase-Setup');
91 - parent.debug('email', 'CreateFirebase-Setup');
92 -
93 - // EXAMPLE
94 - //var payload = { notification: { title: command.title, body: command.msg }, data: { url: obj.msgurl } };
95 - //var options = { priority: 'High', timeToLive: 5 * 60 }; // TTL: 5 minutes, priority 'Normal' or 'High'
96 -
48 +
49 + // Function to send notifications
50 obj.sendToDevice = function (node, payload, options, func) {
98 - if (typeof node == 'string') {
99 - 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'); } })
51 + if (typeof node === 'string') {
52 + parent.db.Get(node, function (err, docs) {
53 + if (!err && docs && docs.length === 1) {
54 + obj.sendToDeviceEx(docs[0], payload, options, func);
55 + } else {
56 + func(0, 'error');
57 + }
58 + });
59 } else {
60 obj.sendToDeviceEx(node, payload, options, func);
61 }
103 - }
104 -
62 + };
63 +
64 // Send an outbound push notification
65 obj.sendToDeviceEx = function (node, payload, options, func) {
107 - parent.debug('email', 'Firebase-sendToDevice');
108 - if ((node == null) || (typeof node.pmt != 'string')) return;
66 + if (!node || typeof node.pmt !== 'string') {
67 + func(0, 'error');
68 + return;
69 + }
70 +
71 obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
110 -
72 +
73 // Fill in our lookup table
112 - if (node._id != null) { tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } }
113 -
114 - // Built the on-screen notification
115 - var notification = null;
116 - if (payload.notification) {
117 - var notification = new Notification('ic_message')
118 - .title(payload.notification.title)
119 - .body(payload.notification.body)
120 - .build();
74 + if (node._id) {
75 + tokenToNodeMap[node.pmt] = {
76 + nid: node._id,
77 + mid: node.meshid,
78 + did: node.domain
79 + };
80 }
122 -
123 - // Build the message
124 - var message = new Message('msg_' + (++obj.messageId));
125 - if (options.priority) { message.priority(options.priority); }
126 - if (payload.data) { for (var i in payload.data) { message.addData(i, payload.data[i]); } }
127 - 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.
128 - if (notification) { message.notification(notification) }
129 - message.build();
130 -
131 - // Send the message
132 - function callback(result) {
133 - if (result.getError() == null) { obj.stats.sent++; obj.log('Success'); } else { obj.stats.sendError++; obj.log('Fail'); }
134 - callback.func(result.getMessageId(), result.getError(), result.getErrorDescription())
135 - }
136 - callback.func = func;
137 - parent.debug('email', 'Firebase-sending');
138 - xcs.sendNoRetry(message, node.pmt, callback);
139 - }
140 -
81 +
82 + const message = {
83 + token: node.pmt,
84 + notification: payload.notification,
85 + data: payload.data,
86 + android: {
87 + priority: options.priority || 'high',
88 + ttl: options.timeToLive ? options.timeToLive * 1000 : undefined
89 + }
90 + };
91 +
92 + admin.messaging().send(message).then(function (response) {
93 + obj.stats.sent++;
94 + obj.log('Success');
95 + func(response);
96 + }).catch(function (error) {
97 + obj.stats.sendError++;
98 + obj.log('Fail: ' + error);
99 + func(0, error);
100 + });
101 + };
102 +
103 // Setup a two way relay
104 obj.setupRelay = function (ws) {
143 - // Select and set a relay identifier
105 ws.relayId = getRandomPassword();
145 - while (obj.relays[ws.relayId] != null) { ws.relayId = getRandomPassword(); }
106 + while (obj.relays[ws.relayId]) { ws.relayId = getRandomPassword(); }
107 obj.relays[ws.relayId] = ws;
108
148 - // On message, parse it
109 ws.on('message', function (msg) {
110 parent.debug('email', 'FBWS-Data(' + this.relayId + '): ' + msg);
151 - if (typeof msg == 'string') {
111 + if (typeof msg === 'string') {
112 obj.log('Relay: ' + msg);
153 -
154 - // Parse the incoming push request
155 - var data = null;
156 - try { data = JSON.parse(msg) } catch (ex) { return; }
157 - if (typeof data != 'object') return;
158 - if (parent.common.validateObjectForMongo(data, 4096) == false) return; // Perform sanity checking on this object.
159 - if (typeof data.pmt != 'string') return;
160 - if (typeof data.payload != 'object') return;
161 - if (typeof data.payload.notification == 'object') {
162 - if (typeof data.payload.notification.title != 'string') return;
163 - if (typeof data.payload.notification.body != 'string') return;
164 - }
165 - if (typeof data.options != 'object') return;
166 - if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) return;
167 - if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) return;
168 - if (typeof data.payload.data != 'object') { data.payload.data = {}; }
169 - data.payload.data.r = ws.relayId; // Set the relay id.
170 -
171 - // Send the push notification
172 - obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
173 - if (err == null) {
174 - try { wsrelay.send(JSON.stringify({ sent: true })); } catch (ex) { }
113 +
114 + let data;
115 + try { data = JSON.parse(msg); } catch (ex) { return; }
116 + if (typeof data !== 'object') return;
117 + if (!parent.common.validateObjectForMongo(data, 4096)) return;
118 + if (typeof data.pmt !== 'string' || typeof data.payload !== 'object') return;
119 +
120 + data.payload.data = data.payload.data || {};
121 + data.payload.data.r = ws.relayId;
122 +
123 + obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err) {
124 + if (!err) {
125 + try { ws.send(JSON.stringify({ sent: true })); } catch (ex) { }
126 } else {
176 - try { wsrelay.send(JSON.stringify({ sent: false })); } catch (ex) { }
127 + try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
128 }
129 });
130 }
131 });
181 -
132 +
133 // If error, close the relay
134 ws.on('error', function (err) {
135 parent.debug('email', 'FBWS-Error(' + this.relayId + '): ' + err);
136 delete obj.relays[this.relayId];
137 });
187 -
138 +
139 // Close the relay
140 ws.on('close', function () {
141 parent.debug('email', 'FBWS-Close(' + this.relayId + ')');
142 delete obj.relays[this.relayId];
143 });
193 -
144 + };
145 +
146 + function getRandomPassword() {
147 + return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').replace(/\//g, '@');
148 }
195 -
196 - function getRandomPassword() { return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
197 -
149 +
150 return obj;
151 };
152
@@ -216,7 +168,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
168 const querystring = require('querystring');
169 const relayUrl = require('url').parse(url);
170 parent.debug('email', 'CreateFirebaseRelay-Setup');
219 -
171 +
172 // Setup logging
173 if (parent.config.firebaserelay && (parent.config.firebaserelay.log === true)) {
174 obj.logpath = parent.path.join(parent.datapath, 'firebaserelay.txt');
@@ -224,7 +176,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
176 } else {
177 obj.log = function () { }
178 }
227 -
179 +
180 obj.log('Starting relay to: ' + relayUrl.href);
181 if (relayUrl.protocol == 'wss:') {
182 // Setup two-way push notification channel
@@ -256,7 +208,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
208 parent.debug('email', 'FBWS-Disconnected');
209 obj.wsclient = null;
210 obj.wsopen = false;
259 -
211 +
212 // Compute the backoff timer
213 if (obj.reconnectTimer == null) {
214 if ((obj.lastConnect != null) && ((Date.now() - obj.lastConnect) > 10000)) { obj.backoffTimer = 0; }
@@ -267,12 +219,12 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
219 }
220 });
221 }
270 -
222 +
223 function processMessage(messageId, from, data, category) {
224 // Lookup node information from the cache
225 var ninfo = obj.tokenToNodeMap[from];
226 if (ninfo == null) { obj.stats.receivedNoRoute++; return; }
275 -
227 +
228 if ((data != null) && (data.con != null) && (data.s != null)) { // Console command
229 obj.stats.received++;
230 parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid);
@@ -280,7 +232,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
232 obj.stats.receivedBadArgs++;
233 }
234 }
283 -
235 +
236 obj.sendToDevice = function (node, payload, options, func) {
237 if (typeof node == 'string') {
238 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'); } })
@@ -288,19 +240,19 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
240 obj.sendToDeviceEx(node, payload, options, func);
241 }
242 }
291 -
243 +
244 obj.sendToDeviceEx = function (node, payload, options, func) {
245 parent.debug('email', 'Firebase-sendToDevice-webSocket');
246 if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; }
247 obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
296 -
248 +
249 // Fill in our lookup table
250 if (node._id != null) { obj.tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } }
299 -
251 +
252 // Fill in the server agent cert hash
253 if (payload.data == null) { payload.data = {}; }
254 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.
303 -
255 +
256 // If the web socket is open, send now
257 if (obj.wsopen == true) {
258 try { obj.wsclient.send(JSON.stringify({ pmt: node.pmt, payload: payload, options: options })); } catch (ex) { func(0, 'error'); obj.stats.sendError++; return; }
@@ -318,7 +270,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
270 } else if (relayUrl.protocol == 'https:') {
271 // Send an outbound push notification using an HTTPS POST
272 obj.pushOnly = true;
321 -
273 +
274 obj.sendToDevice = function (node, payload, options, func) {
275 if (typeof node == 'string') {
276 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'); } })
@@ -326,18 +278,18 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
278 obj.sendToDeviceEx(node, payload, options, func);
279 }
280 }
329 -
281 +
282 obj.sendToDeviceEx = function (node, payload, options, func) {
283 parent.debug('email', 'Firebase-sendToDevice-httpPost');
284 if ((node == null) || (typeof node.pmt != 'string')) return;
333 -
285 +
286 // Fill in the server agent cert hash
287 if (payload.data == null) { payload.data = {}; }
288 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.
337 -
289 +
290 obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
291 const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) });
340 -
292 +
293 // Send the message to the relay
294 const httpOptions = {
295 hostname: relayUrl.hostname,
@@ -361,6 +313,6 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
313 req.end();
314 }
315 }
364 -
316 +
317 return obj;
318 };
\ No newline at end of file
meshcentral.js
+17 -21
@@ -1998,25 +1998,17 @@ function CreateMeshCentralServer(config, args) {
1998
1999 // Setup Firebase
2000 if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
2001 - if (nodeVersion >= 23) {
2002 - addServerWarning('Firebase is not supported on this version of NodeJS.', 27);
2003 - } else {
2004 - obj.firebase = require('./firebase').CreateFirebase(obj, config.firebase.senderid, config.firebase.serverkey);
2005 - }
2001 + addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
2002 + } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
2003 + var serviceAccount;
2004 + try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
2005 + if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
2006 } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
2007 - if (nodeVersion >= 23) {
2008 - addServerWarning('Firebase is not supported on this version of NodeJS.', 27);
2009 - } else {
2010 - // Setup the push messaging relay
2011 - obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
2012 - }
2007 + // Setup the push messaging relay
2008 + obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
2009 } else if (obj.config.settings.publicpushnotifications === true) {
2014 - if (nodeVersion >= 23) {
2015 - addServerWarning('Firebase is not supported on this version of NodeJS.', 27);
2016 - } else {
2017 - // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
2018 - obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
2019 - }
2010 + // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
2011 + obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
2012 }
2013
2014 // Start periodic maintenance
@@ -4049,7 +4041,12 @@ function InstallModules(modules, args, func) {
4041 try {
4042 // Does the module need a specific version?
4043 if (moduleVersion) {
4052 - if (require(`${moduleName}/package.json`).version != moduleVersion) { throw new Error(); }
4044 + var versionMatch = false;
4045 + try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) { }
4046 + if (versionMatch == false) {
4047 + const packageJson = JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'node_modules', moduleName, 'package.json'), 'utf8'));
4048 + if (packageJson.version != moduleVersion) { throw new Error(); }
4049 + }
4050 } else {
4051 // For all other modules, do the check here.
4052 // Is the module in package.json? Install exact version.
@@ -4129,7 +4126,7 @@ var ServerWarnings = {
4126 24: "Unable to load agent logo file: {0}.",
4127 25: "This NodeJS version does not support OpenID.",
4128 26: "This NodeJS version does not support Discord.js.",
4132 - 27: "Firebase is not supported on this version of NodeJS."
4129 + 27: "Firebase now requires a service account JSON file, Firebase disabled."
4130 };
4131 */
4132
@@ -4301,8 +4298,7 @@ function mainStart() {
4298 if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); }
4299
4300 // Firebase Support
4304 - // Avoid 0.1.8 due to bugs: https://github.com/guness/node-xcs/issues/43
4305 - if (config.firebase != null) { modules.push('node-xcs@0.1.8'); }
4301 + if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
4302
4303 // Syslog support
4304 if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
views/default.handlebars
+1 -1
@@ -2484,7 +2484,7 @@
2484 24: "Unable to load agent logo file: {0}.",
2485 25: "This NodeJS version does not support OpenID.",
2486 26: "This NodeJS version does not support Discord.js.",
2487 - 27: "Firebase is not supported on this version of NodeJS."
2487 + 27: "Firebase now requires a service account JSON file, Firebase disabled."
2488 };
2489 var x = '';
2490 for (var i in message.warnings) {