EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
multiserver.js
Go to the documentation of this file.
1/**
2* @description MeshCentral Multi-Server Support
3* @author Ylian Saint-Hilaire
4* @copyright Intel Corporation 2018-2022
5* @license Apache-2.0
6* @version v0.0.1
7*/
8
9/*jslint node: true */
10/*jshint node: true */
11/*jshint strict:false */
12/*jshint -W097 */
13/*jshint esversion: 6 */
14'use strict';
15
16// Construct a Mesh Multi-Server object. This is used for MeshCentral-to-MeshCentral communication.
17module.exports.CreateMultiServer = function (parent, args) {
18 var obj = {};
19 const WebSocket = require('ws');
20 obj.parent = parent;
21 obj.crypto = require('crypto');
22 obj.peerConfig = parent.config.peers;
23 obj.forge = require('node-forge');
24 obj.outPeerServers = {}; // Outgoing peer servers
25 obj.peerServers = {}; // All connected servers (in & out). Only present in this list if the connection is setup
26 obj.serverid = null;
27
28 // Create a mesh server module that will connect to other servers
29 obj.CreatePeerOutServer = function (parent, serverid, url) {
30 var obj = {};
31 obj.parent = parent;
32 obj.serverid = serverid;
33 obj.url = url;
34 obj.ws = null;
35 obj.certificates = parent.parent.certificates;
36 obj.common = require('./common.js');
37 obj.forge = require('node-forge');
38 obj.crypto = require('crypto');
39 obj.connectionState = 0;
40 obj.retryTimer = null;
41 obj.retryBackoff = 0;
42 obj.connectHandler = null;
43 obj.webCertificateHash = obj.parent.parent.webserver.webCertificateHash;
44 obj.agentCertificateHashBase64 = obj.parent.parent.webserver.agentCertificateHashBase64;
45 obj.agentCertificateAsn1 = obj.parent.parent.webserver.agentCertificateAsn1;
46 obj.peerServerId = null;
47 obj.authenticated = 0;
48 obj.serverCertHash = null;
49 obj.pendingData = [];
50
51 // Disconnect from the server and/or stop trying
52 obj.stop = function () {
53 obj.connectionState = 0;
54 disconnect();
55 };
56
57 // Make one attempt at connecting to the server
58 function connect() {
59 obj.retryTimer = null;
60 obj.connectionState = 1;
61
62 // Get the web socket setup
63 obj.ws = new WebSocket(obj.url + 'meshserver.ashx', { rejectUnauthorized: false, servername: obj.certificates.CommonName, cert: obj.certificates.agent.cert, key: obj.certificates.agent.key });
64 obj.parent.parent.debug('peer', 'OutPeer ' + obj.serverid + ': Connecting to: ' + url + 'meshserver.ashx');
65
66 // Register the connection failed event
67 obj.ws.on('error', function (error) { obj.parent.parent.debug('peer', 'OutPeer ' + obj.serverid + ': Error: ' + error); disconnect(); });
68 obj.ws.on('close', function () { obj.parent.parent.debug('peer', 'OutPeer ' + obj.serverid + ': Disconnected'); disconnect(); });
69
70 // Register the connection event
71 obj.ws.on('open', function () {
72 obj.parent.parent.debug('peer', 'OutPeer ' + obj.serverid + ': Connected');
73 obj.connectionState |= 2;
74 obj.nonce = obj.crypto.randomBytes(48).toString('binary');
75
76 // Get the peer server's certificate and compute the server public key hash
77 if (obj.ws._socket == null) return;
78 if (obj.url.toLowerCase().startsWith('wss://')) {
79 // We are using TLS, use the certificate hash
80 var serverCert = obj.forge.pki.certificateFromAsn1(obj.forge.asn1.fromDer(obj.ws._socket.getPeerCertificate().raw.toString('binary')));
81 obj.serverCertHash = obj.forge.pki.getPublicKeyFingerprint(serverCert.publicKey, { encoding: 'binary', md: obj.forge.md.sha384.create() });
82 } else {
83 // We are not using TLS, blank out the TLS certificate hash
84 obj.serverCertHash = Buffer.alloc(48).toString('binary');
85 }
86
87 // Start authenticate the peer server by sending a auth nonce & server TLS cert hash.
88 // Send 384 bits SHA384 hash of TLS cert public key + 384 bits nonce
89 obj.ws.send(Buffer.from(obj.common.ShortToStr(1) + obj.serverCertHash + obj.nonce, 'binary')); // Command 1, hash + nonce
90 });
91
92 // If a message is received
93 obj.ws.on('message', function (msg) {
94 if (typeof msg != 'string') { msg = msg.toString('binary'); }
95 if (msg.length < 2) return;
96
97 if (msg.charCodeAt(0) == 123) {
98 if ((obj.connectionState & 4) != 0) { processServerData(msg); } else { obj.pendingData.push(msg); }
99 } else {
100 var cmd = obj.common.ReadShort(msg, 0);
101 switch (cmd) {
102 case 1: {
103 // Server authentication request
104 if (msg.length != 98) { obj.parent.parent.debug('peer', 'OutPeer: Bad server authentication message, length = ' + msg.length + ', should be 98. HEX: ' + Buffer.from(msg.substring(0, 4096), 'binary').toString('hex')); return; }
105
106 // Check that the server hash matches the TLS server certificate public key hash
107 if (obj.url.toLowerCase().startsWith('wss://') && (obj.serverCertHash != msg.substring(2, 50))) { obj.parent.parent.debug('peer', 'OutPeer: Server hash mismatch.'); disconnect(); return; }
108 obj.servernonce = msg.substring(50);
109
110 // Perform the hash signature using the server agent certificate
111 obj.parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) {
112 // Send back our certificate + signature
113 if (obj.ws != null) { obj.ws.send(Buffer.from(obj.common.ShortToStr(2) + obj.common.ShortToStr(obj.agentCertificateAsn1.length) + obj.agentCertificateAsn1 + signature, 'binary')); } // Command 2, certificate + signature
114 });
115
116 break;
117 }
118 case 2: {
119 // Server certificate
120 var certlen = obj.common.ReadShort(msg, 2), serverCert = null;
121 var serverCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----';
122 try { serverCert = obj.forge.pki.certificateFromAsn1(obj.forge.asn1.fromDer(msg.substring(4, 4 + certlen))); } catch (e) { }
123 if (serverCert == null) { obj.parent.parent.debug('peer', 'OutPeer: Invalid server certificate.'); disconnect(); return; }
124 var serverid = Buffer.from(obj.forge.pki.getPublicKeyFingerprint(serverCert.publicKey, { encoding: 'binary', md: obj.forge.md.sha384.create() }), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
125 if (serverid !== obj.agentCertificateHashBase64) { obj.parent.parent.debug('peer', 'OutPeer: Server hash mismatch.'); disconnect(); return; }
126
127 // Server signature, verify it. This is the fast way, without using forge. (TODO: Use accelerator for this?)
128 const verify = obj.parent.crypto.createVerify('SHA384');
129 verify.end(Buffer.from(obj.serverCertHash + obj.nonce + obj.servernonce, 'binary'));
130 if (verify.verify(serverCertPem, Buffer.from(msg.substring(4 + certlen), 'binary')) !== true) { obj.parent.parent.debug('peer', 'OutPeer: Server sign check failed.'); disconnect(); return; }
131
132 // Connection is a success, clean up
133 delete obj.nonce;
134 delete obj.servernonce;
135 obj.serverCertHash = Buffer.from(obj.serverCertHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); // Change this value to base64
136 obj.connectionState |= 4;
137 obj.retryBackoff = 0; // Set backoff connection timer back to fast.
138 obj.parent.parent.debug('peer', 'OutPeer ' + obj.serverid + ': Verified peer connection to ' + obj.url);
139
140 // Send information about our server to the peer
141 if (obj.connectionState == 15) {
142 obj.send({ action: 'info', serverid: obj.parent.serverid, dbid: obj.parent.parent.db.identifier, key: obj.parent.parent.serverKey.toString('hex'), serverCertHash: obj.parent.parent.webserver.webCertificateHashBase64 });
143 for (var i in obj.pendingData) { processServerData(obj.pendingData[i]); } // Process any pending data
144 obj.pendingData = [];
145 }
146 //if ((obj.connectionState == 15) && (obj.connectHandler != null)) { obj.connectHandler(1); }
147 break;
148 }
149 case 4: {
150 // Peer server confirmed authentication, we are allowed to send commands to the server
151 obj.connectionState |= 8;
152 if (obj.connectionState == 15) {
153 obj.send({ action: 'info', serverid: obj.parent.serverid, dbid: obj.parent.parent.db.identifier, key: obj.parent.parent.serverKey.toString('hex'), serverCertHash: obj.parent.parent.webserver.webCertificateHashBase64 });
154 for (var i in obj.pendingData) { processServerData(obj.pendingData[i]); } // Process any pending data
155 obj.pendingData = [];
156 }
157 //if ((obj.connectionState == 15) && (obj.connectHandler != null)) { obj.connectHandler(1); }
158 break;
159 }
160 default: {
161 obj.parent.parent.debug('peer', 'OutPeer ' + obj.serverid + ': Un-handled command: ' + cmd);
162 break;
163 }
164 }
165 }
166 });
167 }
168
169 // Disconnect from the server, if we need to, try again with a delay.
170 function disconnect() {
171 if (obj.authenticated == 3) { obj.parent.ClearPeerServer(obj, obj.peerServerId); obj.authenticated = 0; }
172 if ((obj.connectionState == 15) && (obj.connectHandler != null)) { obj.connectHandler(0); }
173 if (obj.ws != null) { obj.ws.close(); obj.ws = null; }
174 if (obj.retryTimer != null) { clearTimeout(obj.retryTimer); obj.retryTimer = null; }
175 // Re-try connection
176 if (obj.connectionState >= 1) { obj.connectionState = 1; if (obj.retryTimer == null) { obj.retryTimer = setTimeout(connect, getConnectRetryTime()); } }
177 }
178
179 // Get the next retry time in milliseconds
180 function getConnectRetryTime() {
181 // The (random & 0x1FFF) creates a random number between 0 and 4096.
182 if (obj.retryBackoff < 30000) { obj.retryBackoff += ((require('crypto').randomBytes(4).readUInt32BE(0) & 0x1FFF) + 1000); }
183 return obj.retryBackoff;
184 }
185
186 // Send a JSON message to the peer server
187 obj.send = function (msg) {
188 try {
189 if (obj.ws == null || obj.connectionState != 15) { return; }
190 if (typeof msg == 'string') { obj.ws.send(msg); return; }
191 if (typeof msg == 'object') { obj.ws.send(JSON.stringify(msg)); return; }
192 } catch (ex) { }
193 };
194
195 // Process incoming peer server JSON data
196 function processServerData(msg) {
197 var str = msg.toString('utf8'), command = null;
198 if (str[0] == '{') {
199 try { command = JSON.parse(str); } catch (e) { obj.parent.parent.debug('peer', 'Unable to parse server JSON (' + obj.remoteaddr + ').'); return; } // If the command can't be parsed, ignore it.
200 if (command.action == 'info') {
201 if (obj.authenticated != 3) {
202 // We get the peer's serverid and database identifier.
203 if ((command.serverid != null) && (command.dbid != null)) {
204 if (command.serverid == obj.parent.serverid) { console.log('ERROR: Same server ID, trying to peer with self. (' + obj.url + ', ' + command.serverid + ').'); return; }
205 if (command.dbid != obj.parent.parent.db.identifier) { console.log('ERROR: Database ID mismatch. Trying to peer to a server with the wrong database. (' + obj.url + ', ' + command.serverid + ').'); return; }
206 if (obj.url.toLowerCase().startsWith('wss://') && (obj.serverCertHash != command.serverCertHash)) { console.log('ERROR: Outer certificate hash mismatch (2). (' + obj.url + ', ' + command.serverid + ').'); return; }
207 obj.peerServerId = command.serverid;
208 obj.peerServerKey = Buffer.from(command.key, 'hex');
209 obj.authenticated = 3;
210 obj.parent.SetupPeerServer(obj, obj.peerServerId);
211 }
212 }
213 } else if (obj.authenticated == 3) {
214 // Pass the message to the parent object for processing.
215 obj.parent.ProcessPeerServerMessage(obj, obj.peerServerId, command);
216 }
217 }
218 }
219
220 connect();
221 return obj;
222 };
223
224 // Create a mesh server module that received a connection to another server
225 obj.CreatePeerInServer = function (parent, ws, req, tls) {
226 var obj = {};
227 obj.ws = ws;
228 obj.tls = tls;
229 obj.parent = parent;
230 obj.common = require('./common.js');
231 obj.forge = require('node-forge');
232 obj.crypto = require('crypto');
233 obj.authenticated = 0;
234 obj.remoteaddr = obj.ws._socket.remoteAddress;
235 obj.receivedCommands = 0;
236 obj.webCertificateHash = obj.parent.parent.webserver.webCertificateHash;
237 obj.agentCertificateHashBase64 = obj.parent.parent.webserver.agentCertificateHashBase64;
238 obj.agentCertificateAsn1 = obj.parent.parent.webserver.agentCertificateAsn1;
239 obj.infoSent = 0;
240 obj.peerServerId = null;
241 obj.serverCertHash = null;
242 obj.pendingData = [];
243 if (obj.remoteaddr.startsWith('::ffff:')) { obj.remoteaddr = obj.remoteaddr.substring(7); }
244 obj.parent.parent.debug('peer', 'InPeer: Connected (' + obj.remoteaddr + ')');
245
246 // Send a message to the peer server
247 obj.send = function (msg) {
248 try {
249 if (typeof msg == 'string') { obj.ws.send(msg); return; }
250 if (typeof msg == 'object') { obj.ws.send(JSON.stringify(msg)); return; }
251 } catch (ex) { }
252 };
253
254 // Disconnect this server
255 obj.close = function (arg) {
256 if ((arg == 1) || (arg == null)) { try { obj.ws.close(); obj.parent.parent.debug('peer', 'InPeer: Soft disconnect ' + obj.peerServerId + ' (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket
257 if (arg == 2) { try { obj.ws._socket._parent.end(); obj.parent.parent.debug('peer', 'InPeer: Hard disconnect ' + obj.peerServerId + ' (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
258 if (obj.authenticated == 3) { obj.parent.ClearPeerServer(obj, obj.peerServerId); obj.authenticated = 0; }
259 };
260
261 // When data is received from the peer server web socket
262 ws.on('message', function (msg) {
263 if (typeof msg != 'string') { msg = msg.toString('binary'); }
264 if (msg.length < 2) return;
265
266 if (msg.charCodeAt(0) == 123) {
267 if (msg.length < 2) return;
268 if (obj.authenticated >= 2) { processServerData(msg); } else { obj.pendingData.push(msg); }
269 } else if (obj.authenticated < 2) { // We are not authenticated
270 var cmd = obj.common.ReadShort(msg, 0);
271 if (cmd == 1) {
272 // Peer server authentication request
273 if ((msg.length != 98) || ((obj.receivedCommands & 1) != 0)) return;
274 obj.receivedCommands += 1; // Peer server can't send the same command twice on the same connection ever. Block DOS attack path.
275
276 // Check that the server hash matches out own web certificate hash
277 if ((obj.tls == true) && (obj.webCertificateHash != msg.substring(2, 50))) { obj.close(); return; }
278 obj.peernonce = msg.substring(50);
279
280 // Perform the hash signature using the server agent certificate
281 obj.parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) {
282 // Send back our certificate + signature
283 obj.ws.send(Buffer.from(obj.common.ShortToStr(2) + obj.common.ShortToStr(obj.agentCertificateAsn1.length) + obj.agentCertificateAsn1 + signature, 'binary')); // Command 2, certificate + signature
284 });
285
286 // Check the peer server signature if we can
287 if (obj.unauthsign != null) {
288 if (processPeerSignature(obj.unauthsign) == false) { obj.close(); return; } else { completePeerServerConnection(); }
289 }
290 }
291 else if (cmd == 2) {
292 // Peer server certificate
293 if ((msg.length < 4) || ((obj.receivedCommands & 2) != 0)) { obj.parent.parent.debug('peer', 'InPeer: Invalid command 2.'); return; }
294 obj.receivedCommands += 2; // Peer server can't send the same command twice on the same connection ever. Block DOS attack path.
295
296 // Decode the certificate
297 var certlen = obj.common.ReadShort(msg, 2);
298 obj.unauth = {};
299 try { obj.unauth.nodeid = Buffer.from(obj.forge.pki.getPublicKeyFingerprint(obj.forge.pki.certificateFromAsn1(obj.forge.asn1.fromDer(msg.substring(4, 4 + certlen))).publicKey, { encoding: 'binary', md: obj.forge.md.sha384.create() }), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (e) { console.log(e); return; }
300 obj.unauth.nodeCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----';
301
302 // Check the peer server signature if we can
303 if (obj.peernonce == null) {
304 obj.unauthsign = msg.substring(4 + certlen);
305 } else {
306 if (processPeerSignature(msg.substring(4 + certlen)) == false) { obj.parent.parent.debug('peer', 'InPeer: Invalid signature.'); obj.close(); return; }
307 }
308 completePeerServerConnection();
309 }
310 else if (cmd == 3) {
311 if ((msg.length < 56) || ((obj.receivedCommands & 4) != 0)) { obj.parent.parent.debug('peer', 'InPeer: Invalid command 3.'); return; }
312 obj.receivedCommands += 4; // Peer server can't send the same command twice on the same connection ever. Block DOS attack path.
313 completePeerServerConnection();
314 }
315 }
316 });
317
318 // If error, do nothing
319 ws.on('error', function (err) { obj.parent.parent.debug('peer', 'InPeer: Connection Error: ' + err); });
320
321 // If the peer server web socket is closed, clean up.
322 ws.on('close', function (req) { obj.parent.parent.debug('peer', 'InPeer disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); obj.close(0); });
323 // obj.ws._socket._parent.on('close', function (req) { obj.parent.parent.debug('peer', 'Peer server TCP disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); });
324
325 // Start authenticate the peer server by sending a auth nonce & server TLS cert hash.
326 // Send 384 bits SHA382 hash of TLS cert public key + 384 bits nonce
327 obj.nonce = obj.crypto.randomBytes(48).toString('binary');
328 obj.ws.send(Buffer.from(obj.common.ShortToStr(1) + obj.webCertificateHash + obj.nonce, 'binary')); // Command 1, hash + nonce
329
330 // Once we get all the information about an peer server, run this to hook everything up to the server
331 function completePeerServerConnection() {
332 if (obj.authenticated != 1) return;
333 obj.ws.send(Buffer.from(obj.common.ShortToStr(4), 'binary'));
334 obj.send({ action: 'info', serverid: obj.parent.serverid, dbid: obj.parent.parent.db.identifier, key: obj.parent.parent.serverKey.toString('hex'), serverCertHash: obj.parent.parent.webserver.webCertificateHashBase64 });
335 obj.authenticated = 2;
336
337 // Process any pending data that was received before peer authentication
338 for (var i in obj.pendingData) { processServerData(obj.pendingData[i]); }
339 obj.pendingData = null;
340 }
341
342 // Verify the peer server signature
343 function processPeerSignature(msg) {
344 // Verify the signature. This is the fast way, without using forge.
345 const verify = obj.parent.crypto.createVerify('SHA384');
346 verify.end(Buffer.from(obj.parent.parent.webserver.webCertificateHash + obj.nonce + obj.peernonce, 'binary'));
347 if (verify.verify(obj.unauth.nodeCertPem, Buffer.from(msg, 'binary')) !== true) { console.log('Peer sign fail 1'); return false; }
348 if (obj.unauth.nodeid !== obj.agentCertificateHashBase64) { console.log('Peer sign fail 2'); return false; }
349
350 // Connection is a success, clean up
351 obj.nodeid = obj.unauth.nodeid;
352 delete obj.nonce;
353 delete obj.peernonce;
354 delete obj.unauth;
355 if (obj.unauthsign) delete obj.unauthsign;
356 obj.authenticated = 1;
357
358 return true;
359 }
360
361 // Process incoming peer server JSON data
362 function processServerData(msg) {
363 var str = msg.toString('utf8'), command = null;
364 if (str[0] == '{') {
365 try { command = JSON.parse(str); } catch (e) { obj.parent.parent.debug('peer', 'Unable to parse server JSON (' + obj.remoteaddr + ').'); return; } // If the command can't be parsed, ignore it.
366 if (command.action == 'info') {
367 if (obj.authenticated != 3) {
368 // We get the peer's serverid and database identifier.
369 if ((command.serverid != null) && (command.dbid != null)) {
370 if (command.serverid == obj.parent.serverid) { console.log('ERROR: Same server ID, trying to peer with self. (' + obj.remoteaddr + ', ' + command.serverid + ').'); return; }
371 if (command.dbid != obj.parent.parent.db.identifier) { console.log('ERROR: Database ID mismatch. Trying to peer to a server with the wrong database. (' + obj.remoteaddr + ', ' + command.serverid + ').'); return; }
372 if (obj.parent.peerConfig.servers[command.serverid] == null) { console.log('ERROR: Unknown peer serverid: ' + command.serverid + ' (' + obj.remoteaddr + ').'); return; }
373 obj.peerServerId = command.serverid;
374 obj.peerServerKey = Buffer.from(command.key, 'hex');
375 obj.serverCertHash = command.serverCertHash;
376 obj.authenticated = 3;
377 obj.parent.SetupPeerServer(obj, obj.peerServerId);
378 }
379 }
380 } else if (obj.authenticated == 3) {
381 // Pass the message to the parent object for processing.
382 obj.parent.ProcessPeerServerMessage(obj, obj.peerServerId, command);
383 }
384 }
385 }
386
387 return obj;
388 };
389
390 // If we have no peering configuration, don't setup this object
391 if (obj.peerConfig == null) { return null; }
392 obj.serverid = obj.parent.config.peers.serverid;
393 if (obj.serverid == null) { obj.serverid = require("os").hostname().toLowerCase(); } else { obj.serverid = obj.serverid.toLowerCase(); }
394 if (args.serverid != null) { obj.serverid = args.serverid.toLowerCase(); }
395 if (obj.parent.config.peers.servers[obj.serverid] == null) { console.log("Error: Unable to peer with other servers, \"" + obj.serverid + "\" not present in peer servers list."); return null; }
396 //console.log('Server peering ID: ' + obj.serverid);
397
398 // Return the private key of a peer server
399 obj.getServerCookieKey = function (serverid) {
400 var server = obj.peerServers[serverid];
401 if (server && server.peerServerKey) return server.peerServerKey;
402 return null;
403 };
404
405 // Dispatch an event to all other MeshCentral2 peer servers
406 obj.DispatchEvent = function (ids, source, event) {
407 for (var serverid in obj.peerServers) { obj.peerServers[serverid].send({ action: 'bus', ids: ids, event: event }); }
408 };
409
410 // Dispatch a message to other MeshCentral2 peer servers
411 obj.DispatchMessage = function (msg) {
412 for (var serverid in obj.peerServers) { obj.peerServers[serverid].send(msg); }
413 };
414
415 // Dispatch a message to other MeshCentral2 peer servers
416 obj.DispatchMessageSingleServer = function (msg, serverid) {
417 var server = obj.peerServers[serverid];
418 if (server != null) { server.send(msg); }
419 };
420
421 // Attempt to connect to all peers
422 obj.ConnectToPeers = function () {
423 for (var serverId in obj.peerConfig.servers) {
424 // We will only connect to names that are larger then ours. This way, eveyone has one connection to everyone else (no cross-connections).
425 if ((serverId > obj.serverid) && (obj.peerConfig.servers[serverId].url != null) && (obj.outPeerServers[serverId] == null)) {
426 obj.outPeerServers[serverId] = obj.CreatePeerOutServer(obj, serverId, obj.peerConfig.servers[serverId].url);
427 }
428 }
429 };
430
431 // We connected to a peer server, setup everything
432 obj.SetupPeerServer = function (server, peerServerId) {
433 obj.parent.debug('peer', 'Connected to peer server ' + peerServerId + '.');
434 //console.log('Connected to peer server ' + peerServerId + '.');
435 obj.peerServers[peerServerId] = server;
436
437 // Send the list of connections to the peer
438 server.send({ action: 'connectivityTable', connectivityTable: obj.parent.peerConnectivityByNode[obj.parent.serverId] });
439
440 // Send a list of user sessions to the peer
441 server.send({ action: 'sessionsTable', sessionsTable: Object.keys(obj.parent.webserver.wssessions2) });
442 };
443
444 // We disconnected to a peer server, clean up everything
445 obj.ClearPeerServer = function (server, peerServerId) {
446 obj.parent.debug('peer', 'Disconnected from peer server ' + peerServerId + '.');
447 //console.log('Disconnected from peer server ' + peerServerId + '.');
448
449 // Clean up the connectivity state
450 delete obj.peerServers[peerServerId];
451 var oldList = obj.parent.peerConnectivityByNode[peerServerId];
452 obj.parent.peerConnectivityByNode[peerServerId] = {};
453 obj.parent.UpdateConnectivityState(oldList);
454
455 // Clean up the sessions list
456 for (var i in obj.parent.webserver.wsPeerSessions[peerServerId]) { delete obj.parent.webserver.wsPeerSessions2[obj.parent.webserver.wsPeerSessions[peerServerId][i]]; }
457 delete obj.parent.webserver.wsPeerSessions[peerServerId];
458 delete obj.parent.webserver.wsPeerSessions3[peerServerId];
459 obj.parent.webserver.recountSessions(); // Recount all sessions
460 };
461
462 // Process a message coming from a peer server
463 obj.ProcessPeerServerMessage = function (server, peerServerId, msg) {
464 var userid, i;
465 //console.log('ProcessPeerServerMessage', peerServerId, msg.action);
466 switch (msg.action) {
467 case 'mqtt': {
468 if ((obj.parent.mqttbroker != null) && (msg.nodeid != null)) { obj.parent.mqttbroker.publishNoPeers(msg.nodeid, msg.topic, msg.message); } // Dispatch in the MQTT broker
469 break;
470 }
471 case 'bus': {
472 obj.parent.DispatchEvent(msg.ids, null, msg.event, true); // Dispatch the peer event
473 break;
474 }
475 case 'connectivityTable': {
476 obj.parent.peerConnectivityByNode[peerServerId] = msg.connectivityTable;
477 obj.parent.UpdateConnectivityState(msg.connectivityTable);
478 break;
479 }
480 case 'sessionsTable': {
481 obj.parent.webserver.wsPeerSessions[peerServerId] = msg.sessionsTable;
482 var userToSession = {};
483 for (i in msg.sessionsTable) {
484 var sessionid = msg.sessionsTable[i];
485 obj.parent.webserver.wsPeerSessions2[sessionid] = peerServerId;
486 userid = sessionid.split('/').slice(0, 3).join('/'); // Take the sessionid and keep only the userid partion
487 if (userToSession[userid] == null) { userToSession[userid] = [sessionid]; } else { userToSession[userid].push(sessionid); } // UserId -> [ SessionId ]
488 }
489 obj.parent.webserver.wsPeerSessions3[peerServerId] = userToSession; // ServerId --> UserId --> SessionId
490 obj.parent.webserver.recountSessions(); // Recount all sessions
491 break;
492 }
493 case 'sessionStart': {
494 obj.parent.webserver.wsPeerSessions[peerServerId].push(msg.sessionid);
495 obj.parent.webserver.wsPeerSessions2[msg.sessionid] = peerServerId;
496 userid = msg.sessionid.split('/').slice(0, 3).join('/');
497 if (obj.parent.webserver.wsPeerSessions3[peerServerId] == null) { obj.parent.webserver.wsPeerSessions3[peerServerId] = {}; }
498 if (obj.parent.webserver.wsPeerSessions3[peerServerId][userid] == null) { obj.parent.webserver.wsPeerSessions3[peerServerId][userid] = [msg.sessionid]; } else { obj.parent.webserver.wsPeerSessions3[peerServerId][userid].push(msg.sessionid); }
499 obj.parent.webserver.recountSessions(msg.sessionid); // Recount a specific user
500 break;
501 }
502 case 'sessionEnd': {
503 i = obj.parent.webserver.wsPeerSessions[peerServerId].indexOf(msg.sessionid);
504 if (i >= 0) { obj.parent.webserver.wsPeerSessions[peerServerId].splice(i, 1); }
505 delete obj.parent.webserver.wsPeerSessions2[msg.sessionid];
506 userid = msg.sessionid.split('/').slice(0, 3).join('/');
507 if (obj.parent.webserver.wsPeerSessions3[peerServerId][userid] != null) {
508 i = obj.parent.webserver.wsPeerSessions3[peerServerId][userid].indexOf(msg.sessionid);
509 if (i >= 0) {
510 obj.parent.webserver.wsPeerSessions3[peerServerId][userid].splice(i, 1);
511 if (obj.parent.webserver.wsPeerSessions3[peerServerId][userid].length == 0) { delete obj.parent.webserver.wsPeerSessions3[peerServerId][userid]; }
512 }
513 }
514 obj.parent.webserver.recountSessions(msg.sessionid); // Recount a specific user
515 break;
516 }
517 case 'SetConnectivityState': {
518 obj.parent.SetConnectivityState(msg.meshid, msg.nodeid, msg.connectTime, msg.connectType, msg.powerState, peerServerId, msg.extraInfo);
519 break;
520 }
521 case 'ClearConnectivityState': {
522 obj.parent.ClearConnectivityState(msg.meshid, msg.nodeid, msg.connectType, peerServerId, msg.extraInfo);
523 break;
524 }
525 case 'relay': {
526 // Check if there is a waiting session
527 var rsession = obj.parent.webserver.wsrelays[msg.id];
528 if (rsession != null) {
529 // Yes, there is a waiting session, see if we must initiate.
530 if (peerServerId > obj.parent.serverId) {
531 // We must initiate the connection to the peer
532 userid = null;
533 if (rsession.peer1.user != null) { userid = rsession.peer1.user._id; }
534 obj.createPeerRelay(rsession.peer1.ws, rsession.peer1.req, peerServerId, userid);
535 delete obj.parent.webserver.wsrelays[msg.id];
536 }
537 } else {
538 // Add this relay session to the peer relay list
539 obj.parent.webserver.wsPeerRelays[msg.id] = { serverId: peerServerId, time: Date.now() };
540
541 // Clear all relay sessions that are more than 1 minute
542 var oneMinuteAgo = Date.now() - 60000;
543 for (i in obj.parent.webserver.wsPeerRelays) { if (obj.parent.webserver.wsPeerRelays[i].time < oneMinuteAgo) { delete obj.parent.webserver.wsPeerRelays[i]; } }
544 }
545 break;
546 }
547 case 'msg': {
548 if (msg.sessionid != null) {
549 // Route this message to a connected user session
550 if (msg.fromNodeid != null) { msg.nodeid = msg.fromNodeid; delete msg.fromNodeid; }
551 var ws = obj.parent.webserver.wssessions2[msg.sessionid];
552 if (ws != null) { ws.send(JSON.stringify(msg)); }
553 } else if (msg.nodeid != null) {
554 // Route this message to a connected agent
555 if (msg.fromSessionid != null) { msg.sessionid = msg.fromSessionid; delete msg.fromSessionid; }
556 var agent = obj.parent.webserver.wsagents[msg.nodeid];
557 if (agent != null) { delete msg.nodeid; agent.send(JSON.stringify(msg)); } // Remove the nodeid since it's implyed and send the message to the agent
558 } else if (msg.meshid != null) {
559 // Route this message to all users of this mesh
560 if (msg.fromNodeid != null) { msg.nodeid = msg.fromNodeid; delete msg.fromNodeid; }
561 var cmdstr = JSON.stringify(msg);
562 for (userid in obj.parent.webserver.wssessions) { // Find all connected users for this mesh and send the message
563 if (parent.webserver.GetMeshRights(userid, msg.meshid) != 0) { // TODO: Look at what rights are needed for message routing
564 var sessions = obj.parent.webserver.wssessions[userid];
565 // Send the message to all users on this server
566 for (i in sessions) { sessions[i].send(cmdstr); }
567 }
568 }
569 }
570 break;
571 }
572 case 'newIntelAmtPolicy': {
573 // See if any agents for the affected device group is connected, if so, update the Intel AMT policy
574 for (var nodeid in obj.parent.webserver.wsagents) {
575 const agent = obj.parent.webserver.wsagents[nodeid];
576 if (agent.dbMeshKey == msg.meshid) { agent.sendUpdatedIntelAmtPolicy(msg.amtpolicy); }
577 }
578 break;
579 }
580 case 'agentMsgByMeshId': {
581 // See if any agents for the target device group is connected, if so, send the message
582 const jsonCmd = JSON.stringify(msg.command);
583 for (var nodeid in obj.parent.webserver.wsagents) {
584 var agent = obj.parent.webserver.wsagents[nodeid];
585 if (agent.dbMeshKey == msg.meshid) { try { agent.send(jsonCmd); } catch (ex) { } }
586 }
587 break;
588 }
589 case 'agentCommand': {
590 if (msg.nodeid != null) {
591 // Route this message to a connected agent
592 var agent = obj.parent.webserver.wsagents[msg.nodeid];
593 if (agent != null) { agent.send(JSON.stringify(msg.command)); }
594 } else if (msg.meshid != null) {
595 // Route this message to all connected agents of this mesh
596 for (var nodeid in obj.parent.webserver.wsagents) {
597 var agent = obj.parent.webserver.wsagents[nodeid];
598 if (agent.dbMeshKey == msg.meshid) { try { agent.send(JSON.stringify(msg.command)); } catch (ex) { } }
599 }
600 }
601 break;
602 }
603 default: {
604 // Unknown peer server command
605 console.log('Unknown action from peer server ' + peerServerId + ': ' + msg.action + '.');
606 break;
607 }
608 }
609 };
610
611 // Create a tunnel connection to a peer server
612 obj.createPeerRelay = function (ws, req, serverid, user) {
613 var server = obj.peerServers[serverid];
614 if ((server == null) || (server.peerServerKey == null)) { return null; }
615 var cookieKey = server.peerServerKey;
616
617 // Parse the user if needed
618 if (typeof user == 'string') { user = { _id: user, domain: user.split('/')[1] }; }
619
620 // Build the connection URL
621 var path = req.path;
622 if (path[0] == '/') path = path.substring(1);
623 if (path.substring(path.length - 11) == '/.websocket') { path = path.substring(0, path.length - 11); }
624 var queryStr = '';
625 for (var i in req.query) { if (i.toLowerCase() != 'auth') { queryStr += ((queryStr == '') ? '?' : '&') + i + '=' + req.query[i]; } }
626 if (user != null) { queryStr += ((queryStr == '') ? '?' : '&') + 'auth=' + obj.parent.encodeCookie({ userid: user._id, domainid: user.domain, ps: 1 }, cookieKey); }
627 var url = obj.peerConfig.servers[serverid].url + path + queryStr;
628
629 // Setup an connect the web socket
630 var tunnel = obj.createPeerRelayEx(ws, url, serverid);
631 tunnel.connect();
632 };
633
634 // Create a tunnel connection to a peer server
635 // We assume that "ws" is paused already.
636 obj.createPeerRelayEx = function (ws, url, serverid) {
637 var peerTunnel = { parent: obj, ws1: ws, ws2: null, url: url, serverid: serverid };
638
639 peerTunnel.connect = function () {
640 // Get the web socket setup
641 peerTunnel.parent.parent.debug('peer', 'FTunnel ' + peerTunnel.serverid + ': Start connect to ' + peerTunnel.url);
642 peerTunnel.ws2 = new WebSocket(peerTunnel.url, { rejectUnauthorized: false, servername: this.parent.parent.certificates.CommonName, cert: this.parent.parent.certificates.agent.cert, key: this.parent.parent.certificates.agent.key });
643
644 // Register the connection failed event
645 peerTunnel.ws2.on('error', function (error) { peerTunnel.parent.parent.debug('peer', 'FTunnel ' + obj.serverid + ': Connection error'); peerTunnel.close(); });
646
647 // If the peer server web socket is closed, clean up.
648 peerTunnel.ws2.on('close', function (req) { peerTunnel.parent.parent.debug('peer', 'FTunnel disconnect ' + peerTunnel.serverid); peerTunnel.close(); });
649
650 // If a message is received from the peer, Peer ---> Browser (TODO: Pipe this?)
651 peerTunnel.ws2.on('message', function (msg, isBinary) { try { peerTunnel.ws2._socket.pause(); peerTunnel.ws1.send((isBinary ? msg : msg.toString('binary')), function () { peerTunnel.ws2._socket.resume(); }); } catch (e) { } });
652
653 // Register the connection event
654 peerTunnel.ws2.on('open', function () {
655 peerTunnel.parent.parent.debug('peer', 'FTunnel ' + peerTunnel.serverid + ': Connected');
656
657 if (peerTunnel.ws2._socket.getPeerCertificate != null) {
658 // Get the peer server's certificate and compute the server public key hash
659 var serverCert = obj.forge.pki.certificateFromAsn1(obj.forge.asn1.fromDer(peerTunnel.ws2._socket.getPeerCertificate().raw.toString('binary')));
660 var serverCertHashHex = Buffer.from(obj.forge.pki.getPublicKeyFingerprint(serverCert.publicKey, { encoding: 'binary', md: obj.forge.md.sha384.create() }), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
661
662 // Check if the peer certificate is the expected one for this serverid
663 if ((obj.peerServers[serverid] == null) || (obj.peerServers[serverid].serverCertHash != serverCertHashHex)) { console.log('ERROR: Outer certificate hash mismatch (1). (' + peerTunnel.url + ', ' + peerTunnel.serverid + ').'); peerTunnel.close(); return; }
664 }
665
666 // Connection accepted, resume the web socket to start the data flow
667 peerTunnel.ws1._socket.resume();
668 });
669
670 // If a message is received from the browser, Browser ---> Peer
671 peerTunnel.ws1.on('message', function (msg) { try { peerTunnel.ws1._socket.pause(); peerTunnel.ws2.send(msg, function () { peerTunnel.ws1._socket.resume(); }); } catch (e) { } });
672
673 // If error, do nothing
674 peerTunnel.ws1.on('error', function (err) { peerTunnel.close(); });
675
676 // If the web socket is closed, close the associated TCP connection.
677 peerTunnel.ws1.on('close', function (req) { peerTunnel.parent.parent.debug('peer', 'FTunnel disconnect ' + peerTunnel.serverid); peerTunnel.close(); });
678 };
679
680 // Disconnect both sides of the tunnel
681 peerTunnel.close = function (arg) {
682 if (arg == 2) {
683 // Hard close, close the TCP socket
684 if (peerTunnel.ws1 != null) { try { peerTunnel.ws1._socket._parent.end(); peerTunnel.parent.parent.debug('peer', 'FTunnel1: Hard disconnect'); } catch (e) { console.log(e); } delete peerTunnel.ws1; }
685 if (peerTunnel.ws2 != null) { try { peerTunnel.ws2._socket._parent.end(); peerTunnel.parent.parent.debug('peer', 'FTunnel2: Hard disconnect'); } catch (e) { console.log(e); } delete peerTunnel.ws2; }
686 } else {
687 // Soft close, close the websocket
688 if (peerTunnel.ws1 != null) { try { peerTunnel.ws1.close(); peerTunnel.parent.parent.debug('peer', 'FTunnel1: Soft disconnect '); } catch (e) { console.log(e); } delete peerTunnel.ws1; }
689 if (peerTunnel.ws2 != null) { try { peerTunnel.ws2.close(); peerTunnel.parent.parent.debug('peer', 'FTunnel2: Soft disconnect '); } catch (e) { console.log(e); } delete peerTunnel.ws2; }
690 }
691 };
692
693 return peerTunnel;
694 };
695
696 setTimeout(function () { obj.ConnectToPeers(); }, 1000); // Delay this a little to make sure we are ready on our side.
697 return obj;
698};