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