Improved push messaging support.

Ylian Saint-Hilaire committed Jan 31, 2021 at 00:18 UTC 0658d6e5a5c264a923fe01ef88fc9a259fa52d89
4 files changed +162 -20
meshrelay.js
+96 -4
@@ -13,6 +13,28 @@
13 /*jshint esversion: 6 */
14 "use strict";
15
16 +// Mesh Rights
17 +const MESHRIGHT_EDITMESH = 0x00000001;
18 +const MESHRIGHT_MANAGEUSERS = 0x00000002;
19 +const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
20 +const MESHRIGHT_REMOTECONTROL = 0x00000008;
21 +const MESHRIGHT_AGENTCONSOLE = 0x00000010;
22 +const MESHRIGHT_SERVERFILES = 0x00000020;
23 +const MESHRIGHT_WAKEDEVICE = 0x00000040;
24 +const MESHRIGHT_SETNOTES = 0x00000080;
25 +const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
26 +const MESHRIGHT_NOTERMINAL = 0x00000200;
27 +const MESHRIGHT_NOFILES = 0x00000400;
28 +const MESHRIGHT_NOAMT = 0x00000800;
29 +const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
30 +const MESHRIGHT_LIMITEVENTS = 0x00002000;
31 +const MESHRIGHT_CHATNOTIFY = 0x00004000;
32 +const MESHRIGHT_UNINSTALL = 0x00008000;
33 +const MESHRIGHT_NODESKTOP = 0x00010000;
34 +const MESHRIGHT_REMOTECOMMAND = 0x00020000;
35 +const MESHRIGHT_RESETOFF = 0x00040000;
36 +const MESHRIGHT_GUESTSHARING = 0x00080000;
37 +const MESHRIGHT_ADMIN = 0xFFFFFFFF;
38
39 function checkDeviceSharePublicIdentifier(parent, domain, nodeid, pid, func) {
40 // Check the public id
@@ -166,7 +188,14 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
188 }
189 }
190 return false;
169 - };
191 + }
192 +
193 + // Push any stored message to the peer
194 + obj.pushStoredMessages = function () {
195 + if ((obj.storedPushedMessages != null) && (this.peer != null)) {
196 + for (var i in obj.storedPushedMessages) { try { this.peer.send(JSON.stringify({ action: 'chat', msg: obj.storedPushedMessages[i] })); } catch (ex) { } }
197 + }
198 + }
199
200 // Send a PING/PONG message
201 function sendPing() {
@@ -277,6 +306,10 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
306 // Do not record the session, just send session start
307 try { ws.send('c'); } catch (ex) { } // Send connect to both peers
308 try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
309 +
310 + // Send any stored push messages
311 + obj.pushStoredMessages();
312 + relayinfo.peer1.pushStoredMessages();
313 return;
314 }
315 }
@@ -300,6 +333,10 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
333 parent.parent.debug('relay', 'Relay: Unable to record to file: ' + recFullFilename);
334 try { ws.send('c'); } catch (ex) { } // Send connect to both peers
335 try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
336 +
337 + // Send any stored push messages
338 + obj.pushStoredMessages();
339 + relayinfo.peer1.pushStoredMessages();
340 } else {
341 // Write the recording file header
342 parent.parent.debug('relay', 'Relay: Started recoding to file: ' + recFullFilename);
@@ -312,10 +349,17 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
349 try { relayinfo.peer1.ws.logfile = ws.logfile = logfile; } catch (ex) {
350 try { ws.send('c'); } catch (ex) { } // Send connect to both peers, 'cr' indicates the session is being recorded.
351 try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
352 + // Send any stored push messages
353 + obj.pushStoredMessages();
354 + relayinfo.peer1.pushStoredMessages();
355 return;
356 }
357 try { ws.send('cr'); } catch (ex) { } // Send connect to both peers, 'cr' indicates the session is being recorded.
358 try { relayinfo.peer1.ws.send('cr'); } catch (ex) { }
359 +
360 + // Send any stored push messages
361 + obj.pushStoredMessages();
362 + relayinfo.peer1.pushStoredMessages();
363 });
364 }
365 });
@@ -324,6 +368,10 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
368 // Send session start
369 try { ws.send('c'); } catch (ex) { } // Send connect to both peers
370 try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
371 +
372 + // Send any stored push messages
373 + obj.pushStoredMessages();
374 + relayinfo.peer1.pushStoredMessages();
375 }
376
377 parent.parent.debug('relay', 'Relay connected: ' + obj.id + ' (' + obj.req.clientIp + ' --> ' + obj.peer.req.clientIp + ')');
@@ -348,9 +396,29 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
396 }
397 } else {
398 // Wait for other relay connection
351 - ws._socket.pause(); // Hold traffic until the other connection
399 + if ((obj.id.startsWith('meshmessenger/node/') == true) && obj.authenticated && (parent.parent.firebase != null)) {
400 + // This is an authenticated messenger session, push messaging may be allowed. Don't hold traffic.
401 + ws._socket.resume(); // Don't hold traffic, process push messages
402 + parent.parent.debug('relay', 'Relay messenger waiting: ' + obj.id + ' (' + obj.req.clientIp + ') ' + (obj.authenticated ? 'Authenticated' : ''));
403 +
404 + // Fetch the Push Messaging Token
405 + const idsplit = obj.id.split('/');
406 + const nodeid = idsplit[1] + '/' + idsplit[2] + '/' + idsplit[3];
407 + parent.db.Get(nodeid, function (err, nodes) {
408 + if ((err == null) && (nodes != null) && (nodes.length == 1) && (typeof nodes[0].pmt == 'string')) {
409 + if ((parent.GetNodeRights(obj.user, nodes[0].meshid, nodes[0]._id) & MESHRIGHT_CHATNOTIFY) != 0) {
410 + obj.pmt = nodes[0].pmt;
411 + obj.nodename = nodes[0].name;
412 + // Create the peer connection URL, we will include that in push messages
413 + obj.msgurl = req.headers.origin + (req.url.split('/.websocket')[0].split('/meshrelay.ashx').join('/messenger')) + '?id=' + req.query.id
414 + }
415 + }
416 + });
417 + } else {
418 + ws._socket.pause(); // Hold traffic until the other connection
419 + parent.parent.debug('relay', 'Relay holding: ' + obj.id + ' (' + obj.req.clientIp + ') ' + (obj.authenticated ? 'Authenticated' : ''));
420 + }
421 parent.wsrelays[obj.id] = { peer1: obj, state: 1, timeout: setTimeout(closeBothSides, 30000) };
353 - parent.parent.debug('relay', 'Relay holding: ' + obj.id + ' (' + obj.req.clientIp + ') ' + (obj.authenticated ? 'Authenticated' : ''));
422
423 // Check if a peer server has this connection
424 if (parent.parent.multiServer != null) {
@@ -372,7 +440,6 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
440
441 // When data is received from the mesh relay web socket
442 ws.on('message', function (data) {
375 - //console.log(typeof data, data.length);
443 if (this.peer != null) {
444 //if (typeof data == 'string') { console.log('Relay: ' + data); } else { console.log('Relay:' + data.length + ' byte(s)'); }
445 if (this.peer.slowRelay == null) {
@@ -403,6 +470,31 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
470 }
471 } catch (ex) { console.log(ex); }
472 }
473 + } else {
474 + if ((typeof data == 'string') && (obj.pmt != null)) {
475 + var command = null;
476 + try { command = JSON.parse(data); } catch (ex) { return; }
477 + if ((typeof command != 'object') || (command.action != 'chat') || (typeof command.msg != 'string') || (command.msg == '')) return;
478 +
479 + // Store pushed messages
480 + if (obj.storedPushedMessages == null) { obj.storedPushedMessages = []; }
481 + obj.storedPushedMessages.push(obj.storedPushedMessages.push(command.msg));
482 + while (obj.storedPushedMessages.length > 50) { obj.storedPushedMessages.shift(); } // Only keep last 50 notifications
483 +
484 + // Send out a push message to the device
485 + command.title = (domain.title ? domain.title : 'MeshCentral');
486 + var payload = { notification: { title: command.title, body: command.msg }, data: { url: obj.msgurl } };
487 + var options = { priority: 'High', timeToLive: 5 * 60 }; // TTL: 5 minutes, priority 'Normal' or 'High'
488 + parent.parent.firebase.messaging().sendToDevice(obj.pmt, payload, options)
489 + .then(function (response) {
490 + parent.parent.debug('email', 'Successfully send push message to device ' + obj.nodename + ', title: ' + command.title + ', msg: ' + command.msg);
491 + try { ws.send(JSON.stringify({ action: 'ctrl', value: 1 })); } catch (ex) { } // Push notification success
492 + })
493 + .catch(function (error) {
494 + parent.parent.debug('email', 'Failed to send push message to device ' + obj.nodename + ', title: ' + command.title + ', msg: ' + command.msg + ', error: ' + error);
495 + try { ws.send(JSON.stringify({ action: 'ctrl', value: 2 })); } catch (ex) { } // Push notification failed
496 + });
497 + }
498 }
499 });
500
meshuser.js
+26 -1
@@ -5203,7 +5203,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5203 db.Get(command.nodeid, function (err, nodes) { // TODO: Make a NodeRights(user) method that also does not do a db call if agent is connected (???)
5204 if ((err == null) && (nodes.length == 1)) {
5205 const node = nodes[0];
5206 - if (((parent.GetMeshRights(user, node.meshid) & MESHRIGHT_REMOTECONTROL) != 0) && (typeof node.pmt == 'string')) {
5206 + if (((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_CHATNOTIFY) != 0) && (typeof node.pmt == 'string')) {
5207 // Send out a push message to the device
5208 var payload = { notification: { title: command.title, body: command.msg } };
5209 var options = { priority: "Normal", timeToLive: 5 * 60 }; // TTL: 5 minutes
@@ -5219,6 +5219,31 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5219 });
5220 break;
5221 }
5222 + case 'pushconsole': {
5223 + // Check if this user has rights on this nodeid
5224 + if (parent.parent.firebase == null) return;
5225 + if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
5226 + if (common.validateString(command.console, 1, 3000) == false) break; // Check console command
5227 + db.Get(command.nodeid, function (err, nodes) { // TODO: Make a NodeRights(user) method that also does not do a db call if agent is connected (???)
5228 + if ((err == null) && (nodes.length == 1)) {
5229 + const node = nodes[0];
5230 + if ((parent.GetNodeRights(user, node.meshid, node._id) == MESHRIGHT_ADMIN) && (typeof node.pmt == 'string')) {
5231 + // Send out a push message to the device
5232 + var payload = { data: { console: command.console, session: ws.sessionId } };
5233 + var options = { priority: "Normal", timeToLive: 60 }; // TTL: 1 minutes, priority 'Normal' or 'High'
5234 + parent.parent.firebase.messaging().sendToDevice(node.pmt, payload, options)
5235 + .then(function (response) {
5236 + try { ws.send(JSON.stringify({ action: 'msg', type: 'console', nodeid: node._id, value: 'OK' })); } catch (ex) { }
5237 + })
5238 + .catch(function (error) {
5239 + try { ws.send(JSON.stringify({ action: 'msg', type: 'console', nodeid: node._id, value: 'Failed: ' + error })); } catch (ex) { }
5240 + parent.parent.debug('email', 'Failed to send push console message to device ' + node.name + ', command: ' + command.console + ', error: ' + error);
5241 + });
5242 + }
5243 + }
5244 + });
5245 + break;
5246 + }
5247 case 'print': {
5248 console.log(command.value);
5249 break;
views/default.handlebars
+26 -8
@@ -823,9 +823,10 @@
823 </td>
824 <td>&nbsp;</td>
825 <td id="p15outputselecttd">
826 - <select id=p15outputselect>
827 - <option value=1>Agent</option>
828 - <option value=2>MQTT</option>
826 + <select id=p15outputselect onchange="setupConsole()">
827 + <option id="p15outputselect1" value=1>Agent</option>
828 + <option id="p15outputselect3" value=3>Push</option>
829 + <option id="p15outputselect2" value=2>MQTT</option>
830 </select>
831 </td>
832 <td style="width:1%"><input id="id_p15consoleClear" type="button" class="bottombutton" value="Clear" onclick="p15consoleClear()" /></td>
@@ -5976,7 +5977,7 @@
5977 x += '<input type=button value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
5978 if ((meshrights & 8) && ((connectivity & 1) || (node.pmt == 1))) { x += '<input type=button value="' + "Message" + '" title="' + "Display a text message on the remote device" + '" onclick=deviceMessageFunction() />'; }
5979 //if ((connectivity & 1) && (meshrights & 8) && (node.agent.id < 5)) { x += '<input type=button value=Toast title="' + "Display a text message of the remote device" + '" onclick=deviceToastFunction() />'; }
5979 - if ((connectivity & 1) && (meshrights & 8) && (node.agent.id == 14)) { x += '<input type=button value="' + "Chat" + '" title="' + "Open chat window to this computer" + '" onclick=deviceChat(event) />'; }
5980 + if ((meshrights & 8) && (connectivity & 1) || (node.pmt == 1)) { x += '<input type=button value="' + "Chat" + '" title="' + "Open chat window to this computer" + '" onclick=deviceChat(event) />'; }
5981 if ((serverinfo.guestdevicesharing !== false) && (node.agent != null) && (node.agent.caps & 3) && (connectivity & 1) && (meshrights & 0x80008) && ((meshrights == 0xFFFFFFFF) || ((meshrights & 0x1000) == 0))) { x += '<input type=button value="' + "Share" + '" title="' + "Create a link to share this device with a guest" + '" onclick=showShareDevice() />'; }
5982
5983 // Custom UI
@@ -6302,6 +6303,7 @@
6303 if (xxdialogMode) return;
6304 var url = '/messenger?id=meshmessenger/' + encodeURIComponentEx(currentNode._id) + '/' + encodeURIComponentEx(userinfo._id) + '&title=' + currentNode.name;
6305 if ((authCookie != null) && (authCookie != '')) { url += '&auth=' + authCookie; }
6306 + if (currentNode.pmt == 1) { url += '&pmt=1'; } // Push messaging is possible for this device
6307 if (e && (e.shiftKey == true)) {
6308 safeNewWindow(url, 'meshmessenger:' + currentNode._id);
6309 } else {
@@ -9333,9 +9335,20 @@
9335 var onlineText = ((consoleNode.conn & 1) != 0) ? "Agent is online" : "Agent is offline"
9336 if ((consoleNode.conn & 16) != 0) { onlineText += ", MQTT is online" }
9337 QH('p15statetext', onlineText);
9336 - QE('p15consoleText', online);
9338 QE('p15uploadCore', ((consoleNode.conn & 1) != 0));
9338 - QV('p15outputselecttd', (consoleNode.conn & 17) == 17);
9339 + QV('p15outputselecttd', ((consoleNode.conn & 16) != 0) || (currentNode.pmt == 1));
9340 + QV('p15outputselect2', ((consoleNode.conn & 16) != 0)); // MQTT channel
9341 + QV('p15outputselect3', (currentNode.pmt == 1)); // Push Notification channel
9342 +
9343 + var c = Q('p15outputselect').value;
9344 + if (((consoleNode.conn & 16) == 0) && (c == 2)) { c = 1; Q('p15outputselect').value = 1; }
9345 + if ((currentNode.pmt != 1) && (c == 3)) { c = 1; Q('p15outputselect').value = 1; }
9346 +
9347 + var active = false;
9348 + if (((consoleNode.conn & 1) != 0) && (c == 1)) { active = true; } // Agent
9349 + if (((consoleNode.conn & 16) != 0) && (c == 2)) { active = true; } // MQTT
9350 + if ((consoleNode.pmt == 1) && (c == 3)) { active = true; } // Push
9351 + QE('p15consoleText', active);
9352 } else {
9353 QH('p15statetext', "Access Denied");
9354 QE('p15consoleText', false);
@@ -9368,12 +9381,17 @@
9381 consoleServerText += t;
9382 meshserver.send({ action: 'serverconsole', value: v });
9383 } else {
9371 - if (((consoleNode.conn & 16) != 0) && ((Q('p15outputselect').value == 2) || ((consoleNode.conn & 1) == 0))) {
9384 + if (((consoleNode.conn & 16) != 0) && (Q('p15outputselect').value == 2)) {
9385 // Send the command to MQTT
9386 t = '<div style=color:orange>' + "MQTT" + '&gt; ' + EscapeHtml(v) + '<br/></div>';
9387 consoleNode.consoleText += t;
9388 meshserver.send({ action: 'sendmqttmsg', topic: 'console', nodeids: [ consoleNode._id ], msg: v });
9376 - } else {
9389 + } else if ((consoleNode.pmt == 1) && (Q('p15outputselect').value == 3)) {
9390 + // Send the command using push notification
9391 + t = '<div style=color:violet>' + "PUSH" + '&gt; ' + EscapeHtml(v) + '<br/></div>';
9392 + consoleNode.consoleText += t;
9393 + meshserver.send({ action: 'pushconsole', nodeid: consoleNode._id, console: v });
9394 + } else if ((consoleNode.conn & 1) == 0) {
9395 // Send the command to the mesh agent
9396 consoleNode.consoleText += t;
9397 meshserver.send({ action: 'msg', type: 'console', nodeid: consoleNode._id, value: v });
views/messenger.handlebars
+14 -7
@@ -63,6 +63,7 @@
63 var args = XparseUriArgs();
64 if (args.key && (isAlphaNumeric(args.key) == false)) { delete args.key; }
65 if (args.locale && (isAlphaNumeric(args.locale) == false)) { delete args.locale; }
66 + var pushMessaging = (args.pmt == 1);
67
68 // WebRTC sessions and data, audio and video channels
69 var random = Math.random(); // Selected random, larger value initiates WebRTC.
@@ -144,7 +145,7 @@
145 document.onkeypress = function ondockeypress(e) {
146 if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
147 if (notification != null) { notification.close(); notification = null; }
147 - if (state == 2) {
148 + if ((state == 2) || pushMessaging) {
149 if (e.keyCode == 13) {
150 // Return
151 xsend(e);
@@ -229,7 +230,7 @@
230 chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Local" + '> ' + outtext + '\r\n');
231 QA('xmsg', '<div style="clear:both"><div class="localBubble">' + EscapeHtml(outtext) + '</div><div></div></div>');
232 Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
232 - send({ action: 'chat', msg: outtext });
233 + if ((state == 2) || pushMessaging) { send({ action: 'chat', msg: outtext }); }
234 Q('xouttext').value = '';
235 Q('xouttext').focus();
236 }
@@ -239,9 +240,9 @@
240
241 // Update user controls
242 function updateControls() {
242 - QE('sendButton', state == 2);
243 - QE('clearButton', state == 2);
244 - QE('xouttext', state == 2);
243 + QE('sendButton', (state == 2) || pushMessaging);
244 + QE('clearButton', (state == 2) || pushMessaging);
245 + QE('xouttext', (state == 2) || pushMessaging);
246 QV('fileButton', state == 2);
247 QV('camButton', webchannel && webchannel.ok && !localStream && (userMediaSupport == 2));
248 QV('micButton', webchannel && webchannel.ok && !localStream && (userMediaSupport > 0));
@@ -348,7 +349,7 @@
349
350 // Send data over the current transport (WebRTC first)
351 function send(data) {
351 - if (state != 2) return; // If not in connected state, ignore this.
352 + if ((state != 2) && (pushMessaging == false)) return; // If not in connected state, ignore this.
353 if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string.
354 if (webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it.
355 else { if (socket != null) { try { socket.send(data); } catch (ex) { } } } // If a websocket channel is present, use that.
@@ -372,6 +373,12 @@
373 //console.log('RECV', data);
374 switch (data.action) {
375 case 'chat': { displayRemote(data.msg); break; } // Incoming chat message.
376 + case 'ctrl': {
377 + if (data.value == 1) { displayControl("Sent as push notification."); }
378 + else if (data.value == 2) { displayControl("Push notification failed."); }
379 + if (data.msg != null) { displayControl(msg); }
380 + break;
381 + }
382 case 'random': { if (random > data.random) { startWebRTC(0, true); } break; } // If we have a larger random value, we start WebRTC.
383 case 'webRtcSdp': { if (!webrtcSessions[webRtcIdSwitch(data.id)]) { startWebRTC(webRtcIdSwitch(data.id), false); } webRtcHandleOffer(webRtcIdSwitch(data.id), data.sdp); break; } // Remote WebRTC offer or answer.
384 case 'webRtcIce': { var webrtc = webrtcSessions[webRtcIdSwitch(data.id)]; if (webrtc) { try { webrtc.addIceCandidate(new RTCIceCandidate(data.ice)); } catch (ex) { } } break; } // Remote ICE candidate
@@ -683,7 +690,7 @@
690 sendws({ action: 'random', random: random }); // Send a random number. Higher number starts the WebRTC session.
691 return;
692 }
686 - if (state == 2) { processMessage(msg.data, 1); }
693 + if (msg.data[0] == '{') { processMessage(msg.data, 1); }
694 }
695 } else {
696 displayControl("Error: No connection key specified.");