| 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. |
| 17 | module.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 | }; |