diff --git a/agents/MeshCentralRouter.exe b/agents/MeshCentralRouter.exe index 435d4f6c..d85dd484 100644 Binary files a/agents/MeshCentralRouter.exe and b/agents/MeshCentralRouter.exe differ diff --git a/agents/meshcore.js b/agents/meshcore.js index 2c8df3b6..5bebf0b3 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -803,6 +803,13 @@ function handleServerCommand(data) { // Perform manual server TLS certificate checking based on the certificate hash given by the server. woptions.rejectUnauthorized = 0; woptions.checkServerIdentity = function checkServerIdentity(certs) { + /* + try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); } + */ + // If the tunnel certificate matches the control channel certificate, accept the connection try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; } catch (ex) { } try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint) return; } catch (ex) { } diff --git a/firebase.js b/firebase.js index 8fddc412..72add8ad 100644 --- a/firebase.js +++ b/firebase.js @@ -18,6 +18,7 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { var obj = {}; obj.messageId = 0; + obj.relays = {}; obj.stats = { mode: "Real", sent: 0, @@ -36,17 +37,27 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { // Messages received from client (excluding receipts) xcs.on('message', function (messageId, from, data, category) { - //console.log('Firebase-Message', messageId, from, data, category); + parent.debug('email', 'Firebase-Message: ' + JSON.stringify(data)); - // 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); + 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) { } + } } else { - obj.stats.receivedBadArgs++; + // 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++; + } } }); @@ -102,6 +113,61 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { xcs.sendNoRetry(message, node.pmt, callback); } + // 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') { + + // Parse the incoming push request + var data = null; + try { data = JSON.parse(msg) } catch (ex) { return; } + if (typeof data != 'object') return; + 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('@'); } + return obj; }; @@ -118,40 +184,102 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { receivedNoRoute: 0, receivedBadArgs: 0 } - obj.pushOnly = true; + const WebSocket = require('ws'); const https = require('https'); const querystring = require('querystring'); const relayUrl = require('url').parse(url); - parent.debug('email', 'CreateFirebaseRelay-Setup'); - // Send an outbound push notification - obj.sendToDevice = function (node, payload, options, func) { - parent.debug('email', 'Firebase-sendToDevice'); - if ((node == null) || (typeof node.pmt != 'string')) return; + if (relayUrl.protocol == 'wss:') { + // Setup two-way push notification channel + obj.wsopen = false; + obj.tokenToNodeMap = {} // Token --> { nid: nodeid, mid: meshid } + obj.connectWebSocket = function () { + if (obj.wsclient != null) return; + obj.wsclient = new WebSocket(relayUrl.href + (key ? ('?key=' + key) : ''), { rejectUnauthorized: false }) + obj.wsclient.on('open', function () { obj.wsopen = true; }); + obj.wsclient.on('message', function (msg) { + parent.debug('email', 'FBWS-Data(' + msg.length + '): ' + msg); + 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); + }); + obj.wsclient.on('error', function (err) { + obj.wsclient = null; + obj.wsopen = false; + setTimeout(obj.connectWebSocket, 2000); + }); + obj.wsclient.on('close', function () { + obj.wsclient = null; + obj.wsopen = false; + setTimeout(obj.connectWebSocket, 2000); + }); + } - const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) }); + function processMessage(messageId, from, data, category) { + // Lookup node information from the cache + var ninfo = obj.tokenToNodeMap[from]; + if (ninfo == null) { obj.stats.receivedNoRoute++; return; } - // 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 + 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++; } } - const req = https.request(httpOptions, function (res) { - 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(); + + obj.sendToDevice = function (node, payload, options, func) { + parent.debug('email', 'Firebase-sendToDevice-webSocket'); + if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; } + + // Fill in our lookup table + if (node._id != null) { obj.tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } } + + // If the web socket is open, send now + if (obj.wsopen == true) { + try { obj.wsclient.send(JSON.stringify({ pmt: node.pmt, payload: payload, options: options })); } catch (ex) { func(0, 'error'); return; } + func(1); + } else { + // TODO: Buffer the push messages until TTL. + func(0, 'error'); + } + } + obj.connectWebSocket(); + } else if (relayUrl.protocol == 'https:') { + // Send an outbound push notification using an HTTPS POST + obj.pushOnly = true; + obj.sendToDevice = function (node, payload, options, func) { + parent.debug('email', 'Firebase-sendToDevice-httpPost'); + if ((node == null) || (typeof node.pmt != 'string')) return; + + const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) }); + + // 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) { + 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(); + } } return obj; diff --git a/meshagent.js b/meshagent.js index f5660bd3..374dcafc 100644 --- a/meshagent.js +++ b/meshagent.js @@ -1176,7 +1176,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Sent by the agent to update agent information ChangeAgentCoreInfo(command); - if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null)) { + if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) { // Agent update. The recovery core was loaded in the agent, send a command to update the agent parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task // If agent disconnection, complete and exit now. @@ -1489,31 +1489,33 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { break; } case 'agentupdate': { - var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task - // If agent disconnection, complete and exit now. - if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } + if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) { + var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task + // If agent disconnection, complete and exit now. + if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } - // Agent is requesting an agent update - obj.agentCoreUpdateTaskId = taskid; - const url = '*' + require('url').parse(obj.agentExeInfo.url).path; - var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid }; + // Agent is requesting an agent update + obj.agentCoreUpdateTaskId = taskid; + const url = '*' + require('url').parse(obj.agentExeInfo.url).path; + var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid }; - // Add the hash - if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; } + // Add the hash + if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; } - // Add server TLS cert hash - if (isIgnoreHashCheck() == false) { - const tlsCertHash = parent.webCertificateFullHashs[domain.id]; - if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); } + // Add server TLS cert hash + if (isIgnoreHashCheck() == false) { + const tlsCertHash = parent.webCertificateFullHashs[domain.id]; + if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); } + } + + // Send the agent update command + obj.send(JSON.stringify(cmd)); } + func.sessionid = command.sessionid; - // Send the agent update command - obj.send(JSON.stringify(cmd)); + // Agent update. The recovery core was loaded in the agent, send a command to update the agent + parent.parent.taskLimiter.launch(func, null, 1); } - func.sessionid = command.sessionid; - - // Agent update. The recovery core was loaded in the agent, send a command to update the agent - parent.parent.taskLimiter.launch(func, null, 1); break; } case 'agentupdatedownloaded': { diff --git a/webserver.js b/webserver.js index 71baa0a7..cf1bdb86 100644 --- a/webserver.js +++ b/webserver.js @@ -1782,7 +1782,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { parent.debug('email', 'handleFirebasePushOnlyRelayRequest'); if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; } if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; } - if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.firebase.pushrelayserver)) { res.sendStatus(404); return; } + if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; } var data = null; try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; } if (typeof data != 'object') { res.sendStatus(404); return; } @@ -1800,6 +1800,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } + // Called to handle two-way push notification relay request + function handleFirebaseRelayRequest(ws, req) { + parent.debug('email', 'handleFirebaseRelayRequest'); + if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; } + if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; } + if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; } + 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; } + obj.parent.firebase.setupRelay(ws); + } + // Called to process an agent invite request function handleAgentInviteRequest(req, res) { const domain = getDomain(req); @@ -5184,7 +5194,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Setup firebase push only server if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) { if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', handleFirebasePushOnlyRelayRequest); } - if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); /*obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest);*/ } + if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); } } // Setup auth strategies using passport if needed