Added Firebase two-way push notification relay.

Ylian Saint-Hilaire committed Feb 3, 2021 at 21:48 UTC 0d67041751cc7d4c4f8810f5e6862ca7df3d45ec
5 files changed +207 -60
agents/MeshCentralRouter.exe
Binary files a/agents/MeshCentralRouter.exe and b/agents/MeshCentralRouter.exe differ
agents/meshcore.js
+7
@@ -803,6 +803,13 @@ function handleServerCommand(data) {
803 // Perform manual server TLS certificate checking based on the certificate hash given by the server.
804 woptions.rejectUnauthorized = 0;
805 woptions.checkServerIdentity = function checkServerIdentity(certs) {
806 + /*
807 + try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); }
808 + try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); }
809 + try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); }
810 + try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); }
811 + */
812 +
813 // If the tunnel certificate matches the control channel certificate, accept the connection
814 try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; } catch (ex) { }
815 try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint) return; } catch (ex) { }
firebase.js
+163 -35
@@ -18,6 +18,7 @@
18 module.exports.CreateFirebase = function (parent, senderid, serverkey) {
19 var obj = {};
20 obj.messageId = 0;
21 + obj.relays = {};
22 obj.stats = {
23 mode: "Real",
24 sent: 0,
@@ -36,17 +37,27 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) {
37
38 // Messages received from client (excluding receipts)
39 xcs.on('message', function (messageId, from, data, category) {
39 - //console.log('Firebase-Message', messageId, from, data, category);
40 + parent.debug('email', 'Firebase-Message: ' + JSON.stringify(data));
41
41 - // Lookup node information from the cache
42 - var ninfo = tokenToNodeMap[from];
43 - if (ninfo == null) { obj.stats.receivedNoRoute++; return; }
44 -
45 - if ((data != null) && (data.con != null) && (data.s != null)) { // Console command
46 - obj.stats.received++;
47 - parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid);
42 + if (typeof data.r == 'string') {
43 + // Lookup push relay server
44 + parent.debug('email', 'Firebase-RelayRoute: ' + data.r);
45 + const wsrelay = obj.relays[data.r];
46 + if (wsrelay != null) {
47 + delete data.r;
48 + try { wsrelay.send(JSON.stringify({ from: from, data: data, category: category })); } catch (ex) { }
49 + }
50 } else {
49 - obj.stats.receivedBadArgs++;
51 + // Lookup node information from the cache
52 + var ninfo = tokenToNodeMap[from];
53 + if (ninfo == null) { obj.stats.receivedNoRoute++; return; }
54 +
55 + if ((data != null) && (data.con != null) && (data.s != null)) { // Console command
56 + obj.stats.received++;
57 + parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid);
58 + } else {
59 + obj.stats.receivedBadArgs++;
60 + }
61 }
62 });
63
@@ -102,6 +113,61 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) {
113 xcs.sendNoRetry(message, node.pmt, callback);
114 }
115
116 + // Setup a two way relay
117 + obj.setupRelay = function (ws) {
118 + // Select and set a relay identifier
119 + ws.relayId = getRandomPassword();
120 + while (obj.relays[ws.relayId] != null) { ws.relayId = getRandomPassword(); }
121 + obj.relays[ws.relayId] = ws;
122 +
123 + // On message, parse it
124 + ws.on('message', function (msg) {
125 + parent.debug('email', 'FBWS-Data(' + this.relayId + '): ' + msg);
126 + if (typeof msg == 'string') {
127 +
128 + // Parse the incoming push request
129 + var data = null;
130 + try { data = JSON.parse(msg) } catch (ex) { return; }
131 + if (typeof data != 'object') return;
132 + if (typeof data.pmt != 'string') return;
133 + if (typeof data.payload != 'object') return;
134 + if (typeof data.payload.notification == 'object') {
135 + if (typeof data.payload.notification.title != 'string') return;
136 + if (typeof data.payload.notification.body != 'string') return;
137 + }
138 + if (typeof data.options != 'object') return;
139 + if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) return;
140 + if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) return;
141 + if (typeof data.payload.data != 'object') { data.payload.data = {}; }
142 + data.payload.data.r = ws.relayId; // Set the relay id.
143 +
144 + // Send the push notification
145 + obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
146 + if (err == null) {
147 + try { wsrelay.send(JSON.stringify({ sent: true })); } catch (ex) { }
148 + } else {
149 + try { wsrelay.send(JSON.stringify({ sent: false })); } catch (ex) { }
150 + }
151 + });
152 + }
153 + });
154 +
155 + // If error, close the relay
156 + ws.on('error', function (err) {
157 + parent.debug('email', 'FBWS-Error(' + this.relayId + '): ' + err);
158 + delete obj.relays[this.relayId];
159 + });
160 +
161 + // Close the relay
162 + ws.on('close', function () {
163 + parent.debug('email', 'FBWS-Close(' + this.relayId + ')');
164 + delete obj.relays[this.relayId];
165 + });
166 +
167 + }
168 +
169 + function getRandomPassword() { return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
170 +
171 return obj;
172 };
173
@@ -118,40 +184,102 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
184 receivedNoRoute: 0,
185 receivedBadArgs: 0
186 }
121 - obj.pushOnly = true;
187 + const WebSocket = require('ws');
188 const https = require('https');
189 const querystring = require('querystring');
190 const relayUrl = require('url').parse(url);
125 -
191 parent.debug('email', 'CreateFirebaseRelay-Setup');
192
128 - // Send an outbound push notification
129 - obj.sendToDevice = function (node, payload, options, func) {
130 - parent.debug('email', 'Firebase-sendToDevice');
131 - if ((node == null) || (typeof node.pmt != 'string')) return;
193 + if (relayUrl.protocol == 'wss:') {
194 + // Setup two-way push notification channel
195 + obj.wsopen = false;
196 + obj.tokenToNodeMap = {} // Token --> { nid: nodeid, mid: meshid }
197 + obj.connectWebSocket = function () {
198 + if (obj.wsclient != null) return;
199 + obj.wsclient = new WebSocket(relayUrl.href + (key ? ('?key=' + key) : ''), { rejectUnauthorized: false })
200 + obj.wsclient.on('open', function () { obj.wsopen = true; });
201 + obj.wsclient.on('message', function (msg) {
202 + parent.debug('email', 'FBWS-Data(' + msg.length + '): ' + msg);
203 + var data = null;
204 + try { data = JSON.parse(msg) } catch (ex) { }
205 + if (typeof data != 'object') return;
206 + if (typeof data.from != 'string') return;
207 + if (typeof data.data != 'object') return;
208 + if (typeof data.category != 'string') return;
209 + processMessage(data.messageId, data.from, data.data, data.category);
210 + });
211 + obj.wsclient.on('error', function (err) {
212 + obj.wsclient = null;
213 + obj.wsopen = false;
214 + setTimeout(obj.connectWebSocket, 2000);
215 + });
216 + obj.wsclient.on('close', function () {
217 + obj.wsclient = null;
218 + obj.wsopen = false;
219 + setTimeout(obj.connectWebSocket, 2000);
220 + });
221 + }
222
133 - const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) });
134 -
135 - // Send the message to the relay
136 - const httpOptions = {
137 - hostname: relayUrl.hostname,
138 - port: relayUrl.port ? relayUrl.port : 443,
139 - path: relayUrl.path + (key ? ('?key=' + key) : ''),
140 - method: 'POST',
141 - //rejectUnauthorized: false, // DEBUG
142 - headers: {
143 - 'Content-Type': 'application/x-www-form-urlencoded',
144 - 'Content-Length': querydata.length
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; }
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);
231 + } else {
232 + obj.stats.receivedBadArgs++;
233 }
234 }
147 - const req = https.request(httpOptions, function (res) {
148 - if (res.statusCode == 200) { obj.stats.sent++; } else { obj.stats.sendError++; }
149 - if (func != null) { func(++obj.messageId, (res.statusCode == 200) ? null : 'error'); }
150 - });
151 - parent.debug('email', 'Firebase-sending');
152 - req.on('error', function (error) { obj.stats.sent++; func(++obj.messageId, 'error'); });
153 - req.write(querydata);
154 - req.end();
235 +
236 + obj.sendToDevice = function (node, payload, options, func) {
237 + parent.debug('email', 'Firebase-sendToDevice-webSocket');
238 + if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; }
239 +
240 + // Fill in our lookup table
241 + if (node._id != null) { obj.tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } }
242 +
243 + // If the web socket is open, send now
244 + if (obj.wsopen == true) {
245 + try { obj.wsclient.send(JSON.stringify({ pmt: node.pmt, payload: payload, options: options })); } catch (ex) { func(0, 'error'); return; }
246 + func(1);
247 + } else {
248 + // TODO: Buffer the push messages until TTL.
249 + func(0, 'error');
250 + }
251 + }
252 + obj.connectWebSocket();
253 + } else if (relayUrl.protocol == 'https:') {
254 + // Send an outbound push notification using an HTTPS POST
255 + obj.pushOnly = true;
256 + obj.sendToDevice = function (node, payload, options, func) {
257 + parent.debug('email', 'Firebase-sendToDevice-httpPost');
258 + if ((node == null) || (typeof node.pmt != 'string')) return;
259 +
260 + const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) });
261 +
262 + // Send the message to the relay
263 + const httpOptions = {
264 + hostname: relayUrl.hostname,
265 + port: relayUrl.port ? relayUrl.port : 443,
266 + path: relayUrl.path + (key ? ('?key=' + key) : ''),
267 + method: 'POST',
268 + //rejectUnauthorized: false, // DEBUG
269 + headers: {
270 + 'Content-Type': 'application/x-www-form-urlencoded',
271 + 'Content-Length': querydata.length
272 + }
273 + }
274 + const req = https.request(httpOptions, function (res) {
275 + if (res.statusCode == 200) { obj.stats.sent++; } else { obj.stats.sendError++; }
276 + if (func != null) { func(++obj.messageId, (res.statusCode == 200) ? null : 'error'); }
277 + });
278 + parent.debug('email', 'Firebase-sending');
279 + req.on('error', function (error) { obj.stats.sent++; func(++obj.messageId, 'error'); });
280 + req.write(querydata);
281 + req.end();
282 + }
283 }
284
285 return obj;
meshagent.js
+25 -23
@@ -1176,7 +1176,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
1176 // Sent by the agent to update agent information
1177 ChangeAgentCoreInfo(command);
1178
1179 - if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null)) {
1179 + if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
1180 // Agent update. The recovery core was loaded in the agent, send a command to update the agent
1181 parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task
1182 // If agent disconnection, complete and exit now.
@@ -1489,31 +1489,33 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
1489 break;
1490 }
1491 case 'agentupdate': {
1492 - var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
1493 - // If agent disconnection, complete and exit now.
1494 - if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
1495 -
1496 - // Agent is requesting an agent update
1497 - obj.agentCoreUpdateTaskId = taskid;
1498 - const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
1499 - var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
1500 -
1501 - // Add the hash
1502 - if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
1503 -
1504 - // Add server TLS cert hash
1505 - if (isIgnoreHashCheck() == false) {
1506 - const tlsCertHash = parent.webCertificateFullHashs[domain.id];
1507 - if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
1492 + if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
1493 + var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
1494 + // If agent disconnection, complete and exit now.
1495 + if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
1496 +
1497 + // Agent is requesting an agent update
1498 + obj.agentCoreUpdateTaskId = taskid;
1499 + const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
1500 + var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
1501 +
1502 + // Add the hash
1503 + if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
1504 +
1505 + // Add server TLS cert hash
1506 + if (isIgnoreHashCheck() == false) {
1507 + const tlsCertHash = parent.webCertificateFullHashs[domain.id];
1508 + if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
1509 + }
1510 +
1511 + // Send the agent update command
1512 + obj.send(JSON.stringify(cmd));
1513 }
1514 + func.sessionid = command.sessionid;
1515
1510 - // Send the agent update command
1511 - obj.send(JSON.stringify(cmd));
1516 + // Agent update. The recovery core was loaded in the agent, send a command to update the agent
1517 + parent.parent.taskLimiter.launch(func, null, 1);
1518 }
1513 - func.sessionid = command.sessionid;
1514 -
1515 - // Agent update. The recovery core was loaded in the agent, send a command to update the agent
1516 - parent.parent.taskLimiter.launch(func, null, 1);
1519 break;
1520 }
1521 case 'agentupdatedownloaded': {
webserver.js
+12 -2
@@ -1782,7 +1782,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1782 parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
1783 if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
1784 if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
1785 - if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.firebase.pushrelayserver)) { res.sendStatus(404); return; }
1785 + if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
1786 var data = null;
1787 try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
1788 if (typeof data != 'object') { res.sendStatus(404); return; }
@@ -1800,6 +1800,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1800 });
1801 }
1802
1803 + // Called to handle two-way push notification relay request
1804 + function handleFirebaseRelayRequest(ws, req) {
1805 + parent.debug('email', 'handleFirebaseRelayRequest');
1806 + if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
1807 + if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
1808 + if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
1809 + if ((typeof obj.parent.config.firebase.relayserver == 'string') && (req.query.key != obj.parent.config.firebase.relayserver)) { res.sendStatus(404); try { ws.close(); } catch (e) { } return; }
1810 + obj.parent.firebase.setupRelay(ws);
1811 + }
1812 +
1813 // Called to process an agent invite request
1814 function handleAgentInviteRequest(req, res) {
1815 const domain = getDomain(req);
@@ -5184,7 +5194,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5194 // Setup firebase push only server
5195 if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
5196 if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', handleFirebasePushOnlyRelayRequest); }
5187 - if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); /*obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest);*/ }
5197 + if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
5198 }
5199
5200 // Setup auth strategies using passport if needed