UNPKG

meshcentral

Version:

Web based remote computer management server

725 lines (656 loc) • 137 kB
/** * @description MeshCentral MeshAgent communication module * @author Ylian Saint-Hilaire & Bryan Roe * @copyright Intel Corporation 2018-2021 * @license Apache-2.0 * @version v0.0.1 */ /*xjslint node: true */ /*xjslint plusplus: true */ /*xjslint maxlen: 256 */ /*jshint node: true */ /*jshint strict: false */ /*jshint esversion: 6 */ "use strict"; // Construct a MeshAgent object, called upon connection module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { const forge = parent.parent.certificateOperations.forge; const common = parent.parent.common; parent.agentStats.createMeshAgentCount++; parent.parent.debug('agent', 'New agent at ' + req.clientIp + ':' + ws._socket.remotePort); var obj = {}; obj.domain = domain; obj.authenticated = 0; obj.receivedCommands = 0; obj.agentCoreCheck = 0; obj.remoteaddr = req.clientIp; obj.remoteaddrport = obj.remoteaddr + ':' + ws._socket.remotePort; obj.nonce = parent.crypto.randomBytes(48).toString('binary'); //ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive, 4 minutes if (args.agentidletimeout != 0) { ws._socket.setTimeout(args.agentidletimeout, function () { obj.close(1); }); } // Inactivity timeout of 2:30 minutes, by default agent will WebSocket ping every 2 minutes and server will pong back. //obj.nodeid = null; //obj.meshid = null; //obj.dbNodeKey = null; //obj.dbMeshKey = null; //obj.connectTime = null; //obj.agentInfo = null; ws._socket.bytesReadEx = 0; ws._socket.bytesWrittenEx = 0; // Perform data accounting function dataAccounting() { parent.trafficStats.AgentCtrlIn += (ws._socket.bytesRead - ws._socket.bytesReadEx); parent.trafficStats.AgentCtrlOut += (ws._socket.bytesWritten - ws._socket.bytesWrittenEx); ws._socket.bytesReadEx = ws._socket.bytesRead; ws._socket.bytesWrittenEx = ws._socket.bytesWritten; } // Send a message to the mesh agent obj.send = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data), func); } else { ws.send(data, func); } } catch (e) { } }; obj.sendBinary = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data, 'binary'), func); } else { ws.send(data, func); } } catch (e) { } }; // Disconnect this agent obj.close = function (arg) { dataAccounting(); if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket if (arg == 2) { try { ws._socket._parent.end(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Hard close, close the TCP socket // If arg == 3, don't communicate with this agent anymore, but don't disconnect (Duplicate agent). // Stop any current self-share if (obj.guestSharing === true) { removeGuestSharing(); } // Remove this agent from the webserver list if (parent.wsagents[obj.dbNodeKey] == obj) { delete parent.wsagents[obj.dbNodeKey]; parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1, null, { remoteaddrport: obj.remoteaddrport, name: obj.name }); } // Remove this agent from the list of agents with bad web certificates if (obj.badWebCert) { delete parent.wsagentsWithBadWebCerts[obj.badWebCert]; } // Get the current mesh const mesh = parent.meshes[obj.dbMeshKey]; // If this is a temporary or recovery agent, or all devices in this group are temporary, remove the agent (0x20 = Temporary, 0x40 = Recovery) if (((obj.agentInfo) && (obj.agentInfo.capabilities) && ((obj.agentInfo.capabilities & 0x20) || (obj.agentInfo.capabilities & 0x40))) || ((mesh) && (mesh.flags) && (mesh.flags & 1))) { // Delete this node including network interface information and events db.Remove(obj.dbNodeKey); // Remove node with that id db.Remove('if' + obj.dbNodeKey); // Remove interface information db.Remove('nt' + obj.dbNodeKey); // Remove notes db.Remove('lc' + obj.dbNodeKey); // Remove last connect time db.Remove('si' + obj.dbNodeKey); // Remove system information db.Remove('al' + obj.dbNodeKey); // Remove error log last time if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node // Event node deletion parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 }); // Disconnect all connections if needed const state = parent.parent.GetConnectivityState(obj.dbNodeKey); if ((state != null) && (state.connectivity != null)) { if ((state.connectivity & 1) != 0) { parent.wsagents[obj.dbNodeKey].close(); } // Disconnect mesh agent if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(obj.dbNodeKey); } // Disconnect CIRA connection } } // Set this agent as no longer authenticated obj.authenticated = -1; // If we where updating the agent using native method, clean that up. if (obj.agentUpdate != null) { if (obj.agentUpdate.fd) { try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } } parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete delete obj.agentUpdate.buf; delete obj.agentUpdate; } // If we where updating the agent meshcore method, clean that up. if (obj.agentCoreUpdateTaskId != null) { parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId); delete obj.agentCoreUpdateTaskId; } // Perform timer cleanup if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; } if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; } // Perform aggressive cleanup delete obj.name; delete obj.nonce; delete obj.nodeid; delete obj.unauth; delete obj.remoteaddr; delete obj.remoteaddrport; delete obj.meshid; delete obj.connectTime; delete obj.agentInfo; delete obj.agentExeInfo; ws.removeAllListeners(['message', 'close', 'error']); }; // When data is received from the mesh agent web socket ws.on('message', function (msg) { dataAccounting(); if (msg.length < 2) return; if (typeof msg == 'object') { msg = msg.toString('binary'); } // TODO: Could change this entire method to use Buffer instead of binary string if (obj.authenticated == 2) { // We are authenticated if ((obj.agentUpdate == null) && (msg.charCodeAt(0) == 123)) { processAgentData(msg); } // Only process JSON messages if meshagent update is not in progress if (msg.length < 2) return; const cmdid = common.ReadShort(msg, 0); if (cmdid == 11) { // MeshCommand_CoreModuleHash if (msg.length == 4) { ChangeAgentCoreInfo({ 'caps': 0 }); } // If the agent indicated that no core is running, clear the core information string. // Mesh core hash, sent by agent with the hash of the current mesh core. // If we are performing an agent update, don't update the core. if (obj.agentUpdate != null) { return; } // If we are using a custom core, don't try to update it. if (obj.agentCoreCheck == 1000) { obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started. agentCoreIsStable(); return; } // Get the current meshcore hash const agentMeshCoreHash = (msg.length == 52) ? msg.substring(4, 52) : null; // If the agent indicates this is a custom core, we are done. if ((agentMeshCoreHash != null) && (agentMeshCoreHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) { obj.agentCoreCheck = 0; obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started. agentCoreIsStable(); return; } // We need to check if the core is current. Figure out what core we need. var corename = null; if ((obj.agentInfo != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null)) { if ((obj.agentCoreCheck == 1001) || (obj.agentCoreUpdate == true)) { // If the user asked, use the recovery core. corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].rcore; } else if (obj.agentCoreCheck == 1011) { // If the user asked, use the tiny core. corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].tcore; } else if (obj.agentInfo.capabilities & 0x40) { // If this is a recovery agent, use the agent recovery core. corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].arcore; } else { // This is the normal core for this agent type. corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core; } } // If we have a core, use it. if (corename != null) { const meshcorehash = parent.parent.defaultMeshCoresHash[corename]; if (agentMeshCoreHash != meshcorehash) { if ((obj.agentCoreCheck < 5) || (obj.agentCoreCheck == 1001) || (obj.agentCoreCheck == 1011) || (obj.agentCoreUpdate == true)) { if (meshcorehash == null) { // Clear the core obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core parent.agentStats.clearingCoreCount++; parent.parent.debug('agent', "Clearing core"); } else { // Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load. var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename }; // If the agent supports compression, sent the core compressed. if ((obj.agentInfo.capabilities & 0x100) && (parent.parent.defaultMeshCoresDeflate[corename])) { args.core = parent.parent.defaultMeshCoresDeflate[corename]; } // Update new core with task limiting so not to flood the server. This is a high priority task. obj.agentCoreUpdatePending = true; parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { if (obj.authenticated == 2) { // Send the updated core. delete obj.agentCoreUpdatePending; obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core, function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update parent.agentStats.updatingCoreCount++; parent.parent.debug('agent', "Updating core " + argument.name); } else { // This agent is probably disconnected, nothing to do. parent.parent.taskLimiter.completed(taskid); } }, taskLimiterOptions, 0); } obj.agentCoreCheck++; } } else { obj.agentCoreCheck = 0; obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started. agentCoreIsStable(); // No updates needed, agent is ready to go. } } /* // TODO: Check if we have a mesh specific core. If so, use that. var agentMeshCoreHash = null; if (msg.length == 52) { agentMeshCoreHash = msg.substring(4, 52); } if ((agentMeshCoreHash != parent.parent.defaultMeshCoreHash) && (agentMeshCoreHash != parent.parent.defaultMeshCoreNoMeiHash)) { if (obj.agentCoreCheck < 5) { // This check is in place to avoid a looping core update. if (parent.parent.defaultMeshCoreHash == null) { // Update no core obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Command 10, ask mesh agent to clear the core } else { // Update new core if ((parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].amt == true)) { obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreHash + parent.parent.defaultMeshCore); // Command 10, ask mesh agent to set the core (with MEI support) } else { obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreNoMeiHash + parent.parent.defaultMeshCoreNoMei); // Command 10, ask mesh agent to set the core (No MEI) } } obj.agentCoreCheck++; } } else { obj.agentCoreCheck = 0; } */ } else if (cmdid == 12) { // MeshCommand_AgentHash if ((msg.length == 52) && (obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) { const agenthash = msg.substring(4); const agentUpdateMethod = compareAgentBinaryHash(obj.agentExeInfo, agenthash) if (agentUpdateMethod === 2) { // Use meshcore agent update system // Send the recovery core to the agent, if the agent is capable of running one if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) { parent.agentStats.agentMeshCoreBinaryUpdate++; obj.agentCoreUpdate = true; obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Ask to clear the core obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Ask for meshcore hash } } else if (agentUpdateMethod === 1) { // Use native agent update system // Mesh agent update required, do it using task limiter so not to flood the network. Medium priority task. parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } // If agent disconnection, complete and exit now. if (obj.nodeid != null) { parent.parent.debug('agent', "Agent update required, NodeID=0x" + obj.nodeid.substring(0, 16) + ', ' + obj.agentExeInfo.desc); } parent.agentStats.agentBinaryUpdate++; if ((obj.agentExeInfo.data == null) && (((obj.agentInfo.capabilities & 0x100) == 0) || (obj.agentExeInfo.zdata == null))) { // Read the agent from disk parent.fs.open(obj.agentExeInfo.path, 'r', function (err, fd) { if (obj.agentExeInfo == null) return; // Agent disconnected during this call. if (err) { parent.parent.debug('agentupdate', "ERROR: " + err); return console.error(err); } obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), fd: fd, taskid: taskid }; // MeshCommand_CoreModule, ask mesh agent to clear the core. // The new core will only be sent after the agent updates. obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result //console.log("Agent update file open."); obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download // Send the first mesh agent update data block obj.agentUpdate.buf[0] = 0; obj.agentUpdate.buf[1] = 14; obj.agentUpdate.buf[2] = 0; obj.agentUpdate.buf[3] = 1; parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) { if (obj.agentUpdate == null) return; if ((err != null) || (bytesRead == 0)) { // Error reading the agent file, stop here. try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete parent.parent.debug('agentupdate', "ERROR: Unable to read first block of agent binary from disk."); delete obj.agentUpdate.buf; delete obj.agentUpdate; } else { // Send the first block to the agent obj.agentUpdate.ptr += bytesRead; parent.parent.debug('agentupdate', "Sent first block of " + bytesRead + " bytes from disk."); obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block } }); }); } else { // Send the agent from RAM obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), taskid: taskid }; // MeshCommand_CoreModule, ask mesh agent to clear the core. // The new core will only be sent after the agent updates. obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download // Send the first mesh agent update data block obj.agentUpdate.buf[0] = 0; obj.agentUpdate.buf[1] = 14; obj.agentUpdate.buf[2] = 0; obj.agentUpdate.buf[3] = 1; // If agent supports compression, send the compressed agent if possible. if ((obj.agentInfo.capabilities & 0x100) && (obj.agentExeInfo.zdata != null)) { // Send compressed data obj.agentUpdate.agentUpdateData = obj.agentExeInfo.zdata; obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.zhash; } else { // Send uncompressed data obj.agentUpdate.agentUpdateData = obj.agentExeInfo.data; obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.hash; } const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr); if (len > 0) { // Send the first block obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len); obj.agentUpdate.ptr += len; obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block parent.parent.debug('agentupdate', "Sent first block of " + len + " bytes from RAM."); } else { // Error parent.parent.debug('agentupdate', "ERROR: Len of " + len + " is invalid."); parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete delete obj.agentUpdate.buf; delete obj.agentUpdate; } } }, null, 1); } else { // Check the mesh core, if the agent is capable of running one if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) { obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash. } } } } else if (cmdid == 14) { // MeshCommand_AgentBinaryBlock if ((msg.length == 4) && (obj.agentUpdate != null)) { const status = common.ReadShort(msg, 2); if (status == 1) { if (obj.agentExeInfo.data == null) { // Read the agent from disk parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) { if ((obj.agentExeInfo == null) || (obj.agentUpdate == null)) return; // Agent disconnected during this async call. if ((err != null) || (bytesRead < 0)) { // Error reading the agent file, stop here. parent.parent.debug('agentupdate', "ERROR: Unable to read agent #" + obj.agentExeInfo.id + " binary from disk."); try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete delete obj.agentUpdate.buf; delete obj.agentUpdate; } else { // Send the next block to the agent parent.parent.debug('agentupdate', "Sending disk agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + bytesRead + "."); obj.agentUpdate.ptr += bytesRead; if (bytesRead == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, bytesRead + 4)); } // Command 14, mesh agent next data block if ((bytesRead < parent.parent.agentUpdateBlockSize) || (obj.agentUpdate.ptr == obj.agentExeInfo.size)) { parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from disk, ptr=" + obj.agentUpdate.ptr + "."); obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentExeInfo.hash); // Command 13, end mesh agent download, send agent SHA384 hash try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete delete obj.agentUpdate.buf; delete obj.agentUpdate; } } }); } else { // Send the agent from RAM const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr); if (len > 0) { obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len); if (len == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, len + 4)); } // Command 14, mesh agent next data block parent.parent.debug('agentupdate', "Sending RAM agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + len + "."); obj.agentUpdate.ptr += len; } if (obj.agentUpdate.ptr == obj.agentUpdate.agentUpdateData.length) { parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from RAM, ptr=" + obj.agentUpdate.ptr + "."); obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentUpdate.agentUpdateHash); // Command 13, end mesh agent download, send agent SHA384 hash parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete delete obj.agentUpdate.buf; delete obj.agentUpdate; } } } } } else if (cmdid == 15) { // MeshCommand_AgentTag var tag = msg.substring(2); while (tag.charCodeAt(tag.length - 1) == 0) { tag = tag.substring(0, tag.length - 1); } // Remove end-of-line zeros. ChangeAgentTag(tag); } } else if (obj.authenticated < 2) { // We are not authenticated // Check if this is a un-authenticated JSON if (msg.charCodeAt(0) == 123) { var str = msg.toString('utf8'), command = null; if (str[0] == '{') { try { command = JSON.parse(str); } catch (ex) { } // If the command can't be parsed, ignore it. if ((command != null) && (command.action === 'agentName') && (typeof command.value == 'string') && (command.value.length > 0) && (command.value.length < 256)) { obj.agentName = command.value; } } return; } const cmd = common.ReadShort(msg, 0); if (cmd == 1) { // Agent authentication request if ((msg.length != 98) || ((obj.receivedCommands & 1) != 0)) return; obj.receivedCommands += 1; // Agent can't send the same command twice on the same connection ever. Block DOS attack path. if (isIgnoreHashCheck()) { // Send the agent web hash back to the agent // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce obj.sendBinary(common.ShortToStr(1) + msg.substring(2, 50) + obj.nonce); // Command 1, hash + nonce. Use the web hash given by the agent. } else { // Check that the server hash matches our own web certificate hash (SHA384) obj.agentSeenCerthash = msg.substring(2, 50); if ((getWebCertHash(domain) != obj.agentSeenCerthash) && (getWebCertFullHash(domain) != obj.agentSeenCerthash) && (parent.defaultWebCertificateHash != obj.agentSeenCerthash) && (parent.defaultWebCertificateFullHash != obj.agentSeenCerthash)) { if (parent.parent.supportsProxyCertificatesRequest !== false) { obj.badWebCert = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64'); parent.wsagentsWithBadWebCerts[obj.badWebCert] = obj; // Add this agent to the list of of agents with bad web certificates. parent.parent.updateProxyCertificates(false); } parent.agentStats.agentBadWebCertHashCount++; parent.setAgentIssue(obj, "BadWebCertHash: " + Buffer.from(msg.substring(2, 50), 'binary').toString('hex')); parent.parent.debug('agent', 'Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').'); parent.parent.debug('agent', 'Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.'); console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').'); console.log('Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.'); delete obj.agentSeenCerthash; return; } else { // The hash matched one of the acceptable values, send the agent web hash back to the agent // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce // Command 1, hash + nonce. Use the web hash given by the agent. obj.sendBinary(common.ShortToStr(1) + obj.agentSeenCerthash + obj.nonce); } } // Use our server private key to sign the ServerHash + AgentNonce + ServerNonce obj.agentnonce = msg.substring(50, 98); // Check if we got the agent auth confirmation if ((obj.receivedCommands & 8) == 0) { // If we did not get an indication that the agent already validated this server, send the server signature. if (obj.useSwarmCert == true) { // Perform the hash signature using older swarm server certificate parent.parent.certificateOperations.acceleratorPerformSignature(1, msg.substring(2) + obj.nonce, null, function (tag, signature) { // Send back our certificate + signature obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.swarmCertificateAsn1.length) + parent.swarmCertificateAsn1 + signature); // Command 2, certificate + signature }); } else { // Perform the hash signature using the server agent certificate parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) { // Send back our certificate + signature obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.agentCertificateAsn1.length) + parent.agentCertificateAsn1 + signature); // Command 2, certificate + signature }); } } // Check the agent signature if we can if (obj.unauthsign != null) { if (processAgentSignature(obj.unauthsign) == false) { parent.agentStats.agentBadSignature1Count++; parent.setAgentIssue(obj, "BadSignature1"); parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return; } else { completeAgentConnection(); } } } else if (cmd == 2) { // Agent certificate if ((msg.length < 4) || ((obj.receivedCommands & 2) != 0)) return; obj.receivedCommands += 2; // Agent can't send the same command twice on the same connection ever. Block DOS attack path. // Decode the certificate const certlen = common.ReadShort(msg, 2); obj.unauth = {}; try { obj.unauth.nodeid = Buffer.from(forge.pki.getPublicKeyFingerprint(forge.pki.certificateFromAsn1(forge.asn1.fromDer(msg.substring(4, 4 + certlen))).publicKey, { md: forge.md.sha384.create() }).data, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (ex) { console.log(ex); parent.parent.debug('agent', ex); return; } obj.unauth.nodeCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----'; // Check the agent signature if we can if (obj.agentnonce == null) { obj.unauthsign = msg.substring(4 + certlen); } else { if (processAgentSignature(msg.substring(4 + certlen)) == false) { parent.agentStats.agentBadSignature2Count++; parent.setAgentIssue(obj, "BadSignature2"); parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return; } } completeAgentConnection(); } else if (cmd == 3) { // Agent meshid if ((msg.length < 70) || ((obj.receivedCommands & 4) != 0)) return; obj.receivedCommands += 4; // Agent can't send the same command twice on the same connection ever. Block DOS attack path. // Set the meshid obj.agentInfo = {}; obj.agentInfo.infoVersion = common.ReadInt(msg, 2); obj.agentInfo.agentId = common.ReadInt(msg, 6); obj.agentInfo.agentVersion = common.ReadInt(msg, 10); obj.agentInfo.platformType = common.ReadInt(msg, 14); if (obj.agentInfo.platformType > 8 || obj.agentInfo.platformType < 1) { obj.agentInfo.platformType = 1; } if (msg.substring(50, 66) == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0') { obj.meshid = Buffer.from(msg.substring(18, 50), 'binary').toString('hex'); // Older HEX MeshID } else { obj.meshid = Buffer.from(msg.substring(18, 66), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); // New Base64 MeshID } //console.log('MeshID', obj.meshid); obj.agentInfo.capabilities = common.ReadInt(msg, 66); if (msg.length > 70) { const computerNameLen = common.ReadShort(msg, 70); obj.agentInfo.computerName = Buffer.from(msg.substring(72, 72 + computerNameLen), 'binary').toString('utf8'); //console.log('computerName', msg.length, computerNameLen, obj.agentInfo.computerName); } else { obj.agentInfo.computerName = ''; //console.log('computerName-none'); } obj.dbMeshKey = 'mesh/' + domain.id + '/' + obj.meshid; completeAgentConnection(); } else if (cmd == 4) { if ((msg.length < 2) || ((obj.receivedCommands & 8) != 0)) return; obj.receivedCommands += 8; // Agent can't send the same command twice on the same connection ever. Block DOS attack path. // Agent already authenticated the server, wants to skip the server signature - which is great for server performance. } else if (cmd == 5) { // ServerID. Agent is telling us what serverid it expects. Useful if we have many server certificates. if ((msg.substring(2, 34) == parent.swarmCertificateHash256) || (msg.substring(2, 50) == parent.swarmCertificateHash384)) { obj.useSwarmCert = true; } } else if (cmd == 30) { // Agent Commit Date. This is future proofing. Can be used to change server behavior depending on the date range of the agent. try { obj.AgentCommitDate = Date.parse(msg.substring(2)) } catch (ex) { } //console.log('Connected Agent Commit Date: ' + msg.substring(2) + ", " + Date.parse(msg.substring(2))); } } }); // If error, do nothing ws.on('error', function (err) { parent.parent.debug('agent', 'AGENT WSERR: ' + err); console.log('AGENT WSERR: ' + err); obj.close(0); }); // If the mesh agent web socket is closed, clean up. ws.on('close', function (req) { parent.agentStats.agentClose++; if (obj.nodeid != null) { const agentId = (obj.agentInfo && obj.agentInfo.agentId) ? obj.agentInfo.agentId : 'Unknown'; //console.log('Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId); parent.parent.debug('agent', 'Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId); // Log the agent disconnection if we are not testing agent update if (args.agentupdatetest == null) { if (parent.wsagentsDisconnections[obj.nodeid] == null) { parent.wsagentsDisconnections[obj.nodeid] = 1; } else { parent.wsagentsDisconnections[obj.nodeid] = ++parent.wsagentsDisconnections[obj.nodeid]; } } } obj.close(0); }); // Return the mesh for this device, in some cases, we may auto-create the mesh. function getMeshAutoCreate() { var mesh = parent.meshes[obj.dbMeshKey]; // If the mesh was not found and we are in LAN mode, check of the domain can be corrected if ((args.lanonly == true) && (mesh == null)) { var smesh = obj.dbMeshKey.split('/'); for (var i in parent.parent.config.domains) { mesh = parent.meshes['mesh/' + i + '/' + smesh[2]]; if (mesh != null) { obj.domain = domain = parent.parent.config.domains[i]; obj.meshid = smesh[2]; obj.dbMeshKey = 'mesh/' + i + '/' + smesh[2]; obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid; break; } } } if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) { const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser.toLowerCase()]; if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) { // Mesh name is hex instead of base64 const meshname = obj.meshid.substring(0, 18); // Create a new mesh for this device const links = {}; links[adminUser._id] = { name: adminUser.name, rights: 0xFFFFFFFF }; mesh = { type: 'mesh', _id: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', domain: domain.id, links: links }; db.Set(mesh); parent.meshes[obj.dbMeshKey] = mesh; if (adminUser.links == null) adminUser.links = {}; adminUser.links[obj.dbMeshKey] = { rights: 0xFFFFFFFF }; db.SetUser(adminUser); parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [adminUser._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: adminUser.name, meshid: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', action: 'createmesh', links: links, msgid: 55, msgArgs: [obj.meshid], msg: "Created device group: " + obj.meshid, domain: domain.id }); } } else { if ((mesh != null) && (mesh.deleted != null) && (mesh.links)) { // Must un-delete this mesh var ids = parent.CreateMeshDispatchTargets(mesh._id, [obj.dbNodeKey]); // See if users still exists, if so, add links to the mesh for (var userid in mesh.links) { const user = parent.users[userid]; if (user) { if (user.links == null) { user.links = {}; } if (user.links[mesh._id] == null) { user.links[mesh._id] = { rights: mesh.links[userid].rights }; ids.push(user._id); db.SetUser(user); } } } // Send out an event indicating this mesh was "created" parent.parent.DispatchEvent(ids, obj, { etype: 'mesh', meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'createmesh', links: mesh.links, msgid: 56, msgArgs: [mesh._id], msg: "Device group undeleted: " + mesh._id, domain: domain.id }); // Mark the mesh as active delete mesh.deleted; db.Set(mesh); } } return mesh; } // Send a PING/PONG message function sendPing() { obj.send('{"action":"ping"}'); } function sendPong() { obj.send('{"action":"pong"}'); } // Once we get all the information about an agent, run this to hook everything up to the server function completeAgentConnection() { if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection || (obj.agentInfo == null)) { return; } obj.pendingCompleteAgentConnection = true; // Setup the agent PING/PONG timers if ((typeof args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.agentping * 1000); } else if ((typeof args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.agentpong * 1000); } // If this is a recovery agent if (obj.agentInfo.capabilities & 0x40) { // Inform mesh agent that it's authenticated. delete obj.pendingCompleteAgentConnection; obj.authenticated = 2; obj.sendBinary(common.ShortToStr(4)); // Ask for mesh core hash. obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); return; } // Check if we have too many agent sessions if (typeof domain.limits.maxagentsessions == 'number') { // Count the number of agent sessions for this domain var domainAgentSessionCount = 0; for (var i in parent.wsagents) { if (parent.wsagents[i].domain.id == domain.id) { domainAgentSessionCount++; } } // Check if we have too many user sessions if (domainAgentSessionCount >= domain.limits.maxagentsessions) { // Too many, hold the connection. parent.agentStats.agentMaxSessionHoldCount++; return; } } /* // Check that the mesh exists var mesh = parent.meshes[obj.dbMeshKey]; if (mesh == null) { var holdConnection = true; if (typeof domain.orphanagentuser == 'string') { var adminUser = parent.users['user/' + domain.id + '/' + args.orphanagentuser]; if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) { // Create a new mesh for this device holdConnection = false; var links = {}; links[user._id] = { name: adminUser.name, rights: 0xFFFFFFFF }; mesh = { type: 'mesh', _id: obj.dbMeshKey, name: obj.meshid, mtype: 2, desc: '', domain: domain.id, links: links }; db.Set(mesh); parent.meshes[obj.meshid] = mesh; parent.parent.AddEventDispatch(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), ws); if (adminUser.links == null) user.links = {}; adminUser.links[obj.meshid] = { rights: 0xFFFFFFFF }; //adminUser.subscriptions = parent.subscribe(adminUser._id, ws); db.SetUser(user); parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(meshid, [user._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: user.name, meshid: obj.meshid, name: obj.meshid, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id }); } } if (holdConnection == true) { // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours. parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').'); console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').'); return; } } if (mesh.mtype != 2) { // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours. parent.parent.debug('agent', 'Agent connected with