MeshCentral/meshagent.js

2179 lines
137 KiB
JavaScript

/**
* @description MeshCentral MeshAgent communication module
* @author Ylian Saint-Hilaire & Bryan Roe
* @copyright Intel Corporation 2018-2022
* @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 {
if (ws._socket._parent != null)
ws._socket._parent.end();
else
ws._socket.end();
if (obj.nodeid != null) {
parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')');
}
} catch (e) { console.log(e); }
}
// If arg == 2, 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.toString('binary'), 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 invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
return;
}
*/
// Check that the node exists
db.Get(obj.dbNodeKey, function (err, nodes) {
if (obj.agentInfo == null) { return; }
var device, mesh;
// See if this node exists in the database
if ((nodes == null) || (nodes.length == 0)) {
// This device does not exist, use the meshid given by the device
// Check if we already have too many devices for this domain
if (domain.limits && (typeof domain.limits.maxdevices == 'number')) {
db.isMaxType(domain.limits.maxdevices, 'node', domain.id, function (ismax, count) {
if (ismax == true) {
// Too many devices in this domain.
parent.agentStats.maxDomainDevicesReached++;
} else {
// We are under the limit, create the new device.
completeAgentConnection2();
}
});
} else {
completeAgentConnection2();
}
return;
} else {
device = nodes[0];
obj.name = device.name;
// This device exists, meshid given by the device must be ignored, use the server side one.
if ((device.meshid != null) && (device.meshid != obj.dbMeshKey)) {
obj.dbMeshKey = device.meshid;
obj.meshid = device.meshid.split('/')[2];
}
// See if this mesh exists, if it does not we may want to create it.
mesh = getMeshAutoCreate();
// Check if the mesh exists
if (mesh == null) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
parent.agentStats.invalidDomainMesh2Count++;
parent.setAgentIssue(obj, "invalidDomainMesh2");
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;
}
// Check if the mesh is the right type
if (mesh.mtype != 2) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
parent.agentStats.invalidMeshType2Count++;
parent.setAgentIssue(obj, "invalidMeshType2");
parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
return;
}
// Mark when this device connected
obj.connectTime = Date.now();
// Device already exists, look if changes have occured
var changes = [], change = 0, log = 0;
if (device.agent == null) { device.agent = { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }; change = 1; }
if (device.rname != obj.agentInfo.computerName) { device.rname = obj.agentInfo.computerName; change = 1; changes.push('computer name'); }
if (device.agent.ver != obj.agentInfo.agentVersion) { device.agent.ver = obj.agentInfo.agentVersion; change = 1; changes.push('agent version'); }
if (device.agent.id != obj.agentInfo.agentId) { device.agent.id = obj.agentInfo.agentId; change = 1; changes.push('agent type'); }
if ((device.agent.caps & 24) != (obj.agentInfo.capabilities & 24)) { device.agent.caps = obj.agentInfo.capabilities; change = 1; changes.push('agent capabilities'); } // If agent console or javascript support changes, update capabilities
if (mesh.flags && (mesh.flags & 2) && (device.name != obj.agentInfo.computerName)) { device.name = obj.agentInfo.computerName; change = 1; } // We want the server name to be sync'ed to the hostname
if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
if (change == 1) {
// Do some clean up if needed, these values should not be in the database.
if (device.conn != null) { delete device.conn; }
if (device.pwr != null) { delete device.pwr; }
if (device.agct != null) { delete device.agct; }
if (device.cict != null) { delete device.cict; }
// Save the updated device in the database
db.Set(device);
// If this is a temporary device, don't log changes
if (obj.agentInfo.capabilities & 0x20) { log = 0; }
// Event the node change
var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
if (log == 0) { event.nolog = 1; } else { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
}
}
completeAgentConnection3(device, mesh);
});
}
function completeAgentConnection2() {
// See if this mesh exists, if it does not we may want to create it.
var mesh = getMeshAutoCreate();
// Check if the mesh exists
if (mesh == null) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
parent.agentStats.invalidDomainMeshCount++;
parent.setAgentIssue(obj, "invalidDomainMesh");
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;
}
// Check if the mesh is the right type
if (mesh.mtype != 2) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
parent.agentStats.invalidMeshTypeCount++;
parent.setAgentIssue(obj, "invalidMeshType");
parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
return;
}
// Mark when this device connected
obj.connectTime = Date.now();
// This node does not exist, create it.
var agentName = obj.agentName ? obj.agentName : obj.agentInfo.computerName;
var device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: agentName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null };
db.Set(device);
// Event the new node
if (obj.agentInfo.capabilities & 0x20) {
// This is a temporary agent, don't log.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, domain: domain.id, nolog: 1 });
} else {
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, msgid: 57, msgArgs: [obj.agentInfo.computerName, mesh.name], msg: ('Added device ' + obj.agentInfo.computerName + ' to device group ' + mesh.name), domain: domain.id });
}
completeAgentConnection3(device, mesh);
}
function completeAgentConnection3(device, mesh) {
// Check if this agent is already connected
const dupAgent = parent.wsagents[obj.dbNodeKey];
parent.wsagents[obj.dbNodeKey] = obj;
if (dupAgent) {
// Record duplicate agents
if (parent.duplicateAgentsLog[obj.dbNodeKey] == null) {
if (dupAgent.remoteaddr == obj.remoteaddr) {
parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr], count: 1 };
} else {
parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr, dupAgent.remoteaddr], count: 1 };
}
} else {
parent.duplicateAgentsLog[obj.dbNodeKey].name = device.name;
parent.duplicateAgentsLog[obj.dbNodeKey].group = mesh.name;
parent.duplicateAgentsLog[obj.dbNodeKey].count++;
if (parent.duplicateAgentsLog[obj.dbNodeKey].ip.indexOf(obj.remoteaddr) == -1) { parent.duplicateAgentsLog[obj.dbNodeKey].ip.push(obj.remoteaddr); }
}
// Close the duplicate agent
parent.agentStats.duplicateAgentCount++;
parent.setAgentIssue(obj, 'duplicateAgent');
if (obj.nodeid != null) { parent.parent.debug('agent', 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); }
dupAgent.close(3);
} else {
// Indicate the agent is connected
parent.parent.SetConnectivityState(obj.dbMeshKey, obj.dbNodeKey, obj.connectTime, 1, 1, null, { remoteaddrport: obj.remoteaddrport, name: device.name });
}
// We are done, ready to communicate with this agent
delete obj.pendingCompleteAgentConnection;
obj.authenticated = 2;
// Check how many times this agent disconnected in the last few minutes.
const disconnectCount = parent.wsagentsDisconnections[obj.nodeid];
if (disconnectCount > 6) {
parent.parent.debug('agent', 'Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
console.log('Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
parent.agentStats.agentInBigTrouble++;
// TODO: Log or do something to recover?
return;
}
// Command 4, inform mesh agent that it's authenticated.
obj.sendBinary(common.ShortToStr(4));
// Not sure why, but in rare cases, obj.agentInfo is undefined here.
if ((obj.agentInfo == null) || (typeof obj.agentInfo.capabilities != 'number')) { return; } // This is an odd case.
obj.agentExeInfo = parent.parent.meshAgentBinaries[obj.agentInfo.agentId];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[obj.agentInfo.agentId]) { obj.agentExeInfo = domain.meshAgentBinaries[obj.agentInfo.agentId]; }
// Check if this agent is reconnecting too often.
if (disconnectCount > 4) {
// Too many disconnections, this agent has issues. Just clear the core.
obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
parent.parent.debug('agent', 'Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
parent.agentStats.agentInTrouble++;
//console.log('Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
// TODO: Log or do something to recover?
return;
}
// Check if we need to make an native update check
var corename = null;
if (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) {
corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
} else {
// MeshCommand_CoreModule, ask mesh agent to clear the core
obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
}
if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
// Ask the agent for it's executable binary hash
obj.sendBinary(common.ShortToStr(12) + common.ShortToStr(0));
} else {
// Check the mesh core, if the agent is capable of running one
if (((obj.agentInfo.capabilities & 16) != 0) && (corename != null)) {
obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
} else {
agentCoreIsStable(); // No updates needed, agent is ready to go.
}
}
}
// Indicate to the agent that we want to check Intel AMT configuration
// This may trigger a CIRA-LMS tunnel from the agent so the server can inspect the device.
obj.sendUpdatedIntelAmtPolicy = function (policy) {
if (obj.agentExeInfo && (obj.agentExeInfo.amt == true)) { // Only send Intel AMT policy to agents what could have AMT.
if (policy == null) { var mesh = parent.meshes[obj.dbMeshKey]; if (mesh == null) return; policy = mesh.amt; }
if ((policy != null) && (policy.type != 0)) {
const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
}
}
}
function recoveryAgentCoreIsStable(mesh) {
parent.agentStats.recoveryCoreIsStableCount++;
// Recovery agent is doing ok, lets perform main agent checking.
//console.log('recoveryAgentCoreIsStable()');
// Fetch the the real agent nodeid
db.Get('da' + obj.dbNodeKey, function (err, nodes, self) {
if ((nodes != null) && (nodes.length == 1)) {
self.realNodeKey = nodes[0].raid;
// Get agent connection state
var agentConnected = false;
var state = parent.parent.GetConnectivityState(self.realNodeKey);
if (state) { agentConnected = ((state.connectivity & 1) != 0) }
self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: self.realNodeKey, agent: agentConnected } }));
} else {
self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
}
}, obj);
}
function agentCoreIsStable() {
parent.agentStats.coreIsStableCount++;
// Check that the mesh exists
const mesh = parent.meshes[obj.dbMeshKey];
if (mesh == null) {
parent.agentStats.meshDoesNotExistCount++;
parent.setAgentIssue(obj, "meshDoesNotExist");
// TODO: Mark this agent as part of a mesh that does not exists.
return; // Probably not worth doing anything else. Hold this agent.
}
// Check if this is a recovery agent
if (obj.agentInfo.capabilities & 0x40) {
recoveryAgentCoreIsStable(mesh);
return;
}
// Fetch the the diagnostic agent nodeid
db.Get('ra' + obj.dbNodeKey, function (err, nodes) {
if ((nodes != null) && (nodes.length == 1)) {
obj.diagnosticNodeKey = nodes[0].daid;
obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.diagnosticNodeKey } }));
}
});
// Indicate that we want to check the Intel AMT configuration
// This may trigger a CIRA-LMS tunnel to the server for further processing
obj.sendUpdatedIntelAmtPolicy();
// Fetch system information
db.GetHash('si' + obj.dbNodeKey, function (err, results) {
if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
});
// Agent error log dump
if (parent.parent.agentErrorLog != null) {
db.Get('al' + obj.dbNodeKey, function (err, docs) { // Agent Log
if ((docs != null) && (docs.length == 1) && (typeof docs[0].lastEvent)) {
obj.send('{"action":"errorlog","startTime":' + docs[0].lastEvent + '}'); // Ask all events after a given time
} else {
obj.send('{"action":"errorlog"}'); // Ask all
}
});
}
// Set agent core dump
if ((parent.parent.config.settings != null) && ((parent.parent.config.settings.agentcoredump === true) || (parent.parent.config.settings.agentcoredump === false))) {
obj.send(JSON.stringify({ action: 'coredump', value: parent.parent.config.settings.agentcoredump }));
if (parent.parent.config.settings.agentcoredump === true) {
// Check if we requested a core dump file in the last minute, if not, ask if one is present.
if ((parent.lastCoreDumpRequest == null) || ((Date.now() - parent.lastCoreDumpRequest) >= 60000)) { obj.send(JSON.stringify({ action: 'getcoredump' })); }
}
}
// Do this if IP location is enabled on this domain TODO: Set IP location per device group?
if (domain.iplocation == true) {
// Check if we already have IP location information for this node
db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) {
if ((iplocs != null) && (iplocs.length == 1)) {
// We have a location in the database for this remote IP
const iploc = nodes[0], x = {};
if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) {
x.publicip = iploc.ip;
x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000));
ChangeAgentLocationInfo(x);
}
} else {
// Check if we need to ask for the IP location
var doIpLocation = 0;
if (device.iploc == null) {
doIpLocation = 1;
} else {
const loc = device.iploc.split(',');
if (loc.length < 3) {
doIpLocation = 2;
} else {
var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now();
t.setDate(t.getDate() + 20);
if (t < now) { doIpLocation = 3; }
}
}
// If we need to ask for IP location, see if we have the quota to do it.
if (doIpLocation > 0) {
db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) {
if ((ipLocationLimitor != null) && (ipLocationLimitor.value > 0)) {
ipLocationLimitor.value--;
db.Set(ipLocationLimitor);
obj.send(JSON.stringify({ action: 'iplocation' }));
}
});
}
}
});
}
// Indicate server information to the agent.
var serverInfo = { action: 'serverInfo' };
if ((typeof domain.terminal == 'object') && (typeof domain.terminal.launchcommand == 'object')) {
// Send terminal starting command
serverInfo.termlaunchcommand = {};
if (typeof domain.terminal.launchcommand.linux == 'string') { serverInfo.termlaunchcommand.linux = domain.terminal.launchcommand.linux; }
if (typeof domain.terminal.launchcommand.darwin == 'string') { serverInfo.termlaunchcommand.darwin = domain.terminal.launchcommand.darwin; }
if (typeof domain.terminal.launchcommand.freebsd == 'string') { serverInfo.termlaunchcommand.freebsd = domain.terminal.launchcommand.freebsd; }
}
// Enable agent self guest sharing if allowed
if (domain.agentselfguestsharing) { serverInfo.agentSelfGuestSharing = true; }
obj.send(JSON.stringify(serverInfo));
// Plug in handler
if (parent.parent.pluginHandler != null) {
parent.parent.pluginHandler.callHook('hook_agentCoreIsStable', obj, parent);
}
}
// Get the web certificate private key hash for the specified domain
function getWebCertHash(domain) {
const hash = parent.webCertificateHashs[domain.id];
if (hash != null) return hash;
return parent.webCertificateHash;
}
// Get the web certificate hash for the specified domain
function getWebCertFullHash(domain) {
const hash = parent.webCertificateFullHashs[domain.id];
if (hash != null) return hash;
return parent.webCertificateFullHash;
}
// Verify the agent signature
function processAgentSignature(msg) {
if (isIgnoreHashCheck() == false) {
var verified = false;
// This agent did not report a valid TLS certificate hash, fail now.
if (obj.agentSeenCerthash == null) return false;
// Raw RSA signatures have an exact length of 256 or 384. PKCS7 is larger.
if ((msg.length != 384) && (msg.length != 256)) {
// Verify a PKCS7 signature.
var msgDer = null;
try { msgDer = forge.asn1.fromDer(forge.util.createBuffer(msg, 'binary')); } catch (ex) { }
if (msgDer != null) {
try {
const p7 = forge.pkcs7.messageFromAsn1(msgDer);
const sig = p7.rawCapture.signature;
// Verify with key hash
var buf = Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary');
var verifier = parent.crypto.createVerify('RSA-SHA384');
verifier.update(buf);
verified = verifier.verify(obj.unauth.nodeCertPem, sig, 'binary');
if (verified !== true) {
// Not a valid signature
parent.agentStats.invalidPkcsSignatureCount++;
parent.setAgentIssue(obj, "invalidPkcsSignature");
return false;
}
} catch (ex) { };
}
}
if (verified == false) {
// Verify the RSA signature. This is the fast way, without using forge.
const verify = parent.crypto.createVerify('SHA384');
verify.end(Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary')); // Test using the private key hash
if (verify.verify(obj.unauth.nodeCertPem, Buffer.from(msg, 'binary')) !== true) {
parent.agentStats.invalidRsaSignatureCount++;
parent.setAgentIssue(obj, "invalidRsaSignature");
return false;
}
}
}
// Connection is a success, clean up
obj.nodeid = obj.unauth.nodeid;
obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
delete obj.nonce;
delete obj.agentnonce;
delete obj.unauth;
delete obj.receivedCommands;
delete obj.agentSeenCerthash;
if (obj.unauthsign) delete obj.unauthsign;
parent.agentStats.verifiedAgentConnectionCount++;
parent.parent.debug('agent', 'Verified agent connection to ' + obj.nodeid + ' (' + obj.remoteaddrport + ').');
obj.authenticated = 1;
return true;
}
// Process incoming agent JSON data
function processAgentData(msg) {
if (obj.agentInfo == null) return;
var i, 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.
parent.agentStats.invalidJsonCount++;
parent.setAgentIssue(obj, "invalidJson (" + str.length + "): " + str);
parent.parent.debug('agent', 'Unable to parse agent JSON (' + obj.remoteaddrport + ')');
console.log('Unable to parse agent JSON (' + obj.remoteaddrport + '): ' + str, ex);
return;
}
if (typeof command != 'object') { return; }
switch (command.action) {
case 'msg':
{
// If the same console command is processed many times, kick out this agent.
// This is a safety mesure to guard against the agent DOS'ing the server.
if (command.type == 'console') {
if (obj.consoleKickValue == command.value) {
if (obj.consoleKickCount) { obj.consoleKickCount++; } else { obj.consoleKickCount = 1; }
if (obj.consoleKickCount > 30) { obj.close(); return; } // 30 identical console messages received, kick out this agent.
} else {
obj.consoleKickValue = command.value;
}
}
// Route a message
parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
break;
}
case 'coreinfo':
{
// Sent by the agent to update agent information
ChangeAgentCoreInfo(command);
if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
// Agent update. The recovery core was loaded in the agent, send a command to update the agent
parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task
// If agent disconnection, complete and exit now.
if ((obj.authenticated != 2) || (obj.agentExeInfo == null)) { parent.parent.taskLimiter.completed(taskid); return; }
// Agent update. The recovery core was loaded in the agent, send a command to update the agent
obj.agentCoreUpdateTaskId = taskid;
const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex };
parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url);
// Add the hash
if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
// Add server TLS cert hash
if (isIgnoreHashCheck() == false) {
const tlsCertHash = parent.webCertificateFullHashs[domain.id];
if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
}
// Send the agent update command
obj.send(JSON.stringify(cmd));
}, null, 1);
}
break;
}
case 'smbios':
{
// SMBIOS information must never be saved when NeDB is in use. NeDB will currupt that database.
if (db.SetSMBIOS == null) break;
// See if we need to save SMBIOS information
if (domain.smbios === true) {
// Store the RAW SMBios table of this computer
// Perform sanity checks before storing
try {
for (var i in command.value) { var k = parseInt(i); if ((k != i) || (i > 255) || (typeof command.value[i] != 'object') || (command.value[i].length == null) || (command.value[i].length > 1024) || (command.value[i].length < 0)) { delete command.value[i]; } }
db.SetSMBIOS({ _id: obj.dbNodeKey, domain: domain.id, time: new Date(), value: command.value });
} catch (ex) { }
}
// Event the node interface information change (This is a lot of traffic, probably don't need this).
//parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'smBiosChange', nodeid: obj.dbNodeKey, domain: domain.id, smbios: command.value, nolog: 1 });
break;
}
case 'netinfo':
{
// Check if network information is present
if ((command.netif2 == null) && (command.netif == null)) return;
// Escape any field names that have special characters
if (command.netif2 != null) {
for (var i in command.netif2) {
var esc = common.escapeFieldName(i);
if (esc !== i) { command.netif2[esc] = command.netif2[i]; delete command.netif2[i]; }
}
}
// Sent by the agent to update agent network interface information
delete command.action;
command.updateTime = Date.now();
command._id = 'if' + obj.dbNodeKey;
command.domain = domain.id;
command.type = 'ifinfo';
db.Set(command);
// Event the node interface information change
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'ifchange', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
break;
}
case 'iplocation':
{
// Sent by the agent to update location information
if ((command.type == 'publicip') && (command.value != null) && (typeof command.value == 'object') && (command.value.ip) && (command.value.loc)) {
var x = {};
x.publicip = command.value.ip;
x.iploc = command.value.loc + ',' + (Math.floor(Date.now() / 1000));
ChangeAgentLocationInfo(x);
command.value._id = 'iploc_' + command.value.ip;
command.value.type = 'iploc';
command.value.date = Date.now();
db.Set(command.value); // Store the IP to location data in the database
// Sample Value: { ip: '192.55.64.246', city: 'Hillsboro', region: 'Oregon', country: 'US', loc: '45.4443,-122.9663', org: 'AS4983 Intel Corporation', postal: '97123' }
}
break;
}
case 'mc1migration':
{
if (command.oldnodeid.length != 64) break;
const oldNodeKey = 'node//' + command.oldnodeid.toLowerCase();
db.Get(oldNodeKey, function (err, nodes) {
if ((nodes == null) || (nodes.length != 1)) return;
const node = nodes[0];
if (node.meshid == obj.dbMeshKey) {
// Update the device name & host
const newNode = { "name": node.name };
if (node.intelamt != null) { newNode.intelamt = node.intelamt; }
ChangeAgentCoreInfo(newNode);
// Delete this node including network interface information and events
db.Remove(node._id);
db.Remove('if' + node._id);
// Event node deletion
const change = 'Migrated device ' + node.name;
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: node._id, msg: change, domain: node.domain });
}
});
break;
}
case 'openUrl':
{
// Sent by the agent to return the status of a open URL action.
// Nothing is done right now.
break;
}
case 'log':
{
// Log a value in the event log
if ((typeof command.msg == 'string') && (command.msg.length < 4096)) {
var event = { etype: 'node', action: 'agentlog', nodeid: obj.dbNodeKey, domain: domain.id, msg: command.msg };
if (typeof command.msgid == 'number') { event.msgid = command.msgid; }
if (typeof command.guestname == 'string') { event.guestname = command.guestname; }
if (Array.isArray(command.msgArgs)) { event.msgArgs = command.msgArgs; }
if (typeof command.remoteaddr == 'string') { event.remoteaddr = command.remoteaddr; }
var targets = parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]);
if (typeof command.userid == 'string') {
var loguser = parent.users[command.userid];
if (loguser) { event.userid = command.userid; event.username = loguser.name; targets.push(command.userid); }
}
if (typeof command.xuserid == 'string') {
var xloguser = parent.users[command.xuserid];
if (xloguser) { targets.push(command.xuserid); }
}
if ((typeof command.sessionid == 'string') && (command.sessionid.length < 500)) { event.sessionid = command.sessionid; }
parent.parent.DispatchEvent(targets, obj, event);
// If this is a help request, see if we need to email notify anyone
if (event.msgid == 98) {
// Get the node and change it if needed
db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
const device = nodes[0];
if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
});
}
}
break;
}
case 'ping': { sendPong(); break; }
case 'pong': { break; }
case 'getScript':
{
// Used by the agent to get configuration scripts.
if (command.type == 1) {
parent.getCiraConfigurationScript(obj.dbMeshKey, function (script) {
obj.send(JSON.stringify({ action: 'getScript', type: 1, script: script.toString() }));
});
} else if (command.type == 2) {
parent.getCiraCleanupScript(function (script) {
obj.send(JSON.stringify({ action: 'getScript', type: 2, script: script.toString() }));
});
}
break;
}
case 'diagnostic':
{
if (typeof command.value == 'object') {
switch (command.value.command) {
case 'register': {
// Only main agent can do this
if (((obj.agentInfo.capabilities & 0x40) == 0) && (typeof command.value.value == 'string') && (command.value.value.length == 64)) {
// Store links to diagnostic agent id
var daNodeKey = 'node/' + domain.id + '/' + db.escapeBase64(command.value.value);
db.Set({ _id: 'da' + daNodeKey, domain: domain.id, time: obj.connectTime, raid: obj.dbNodeKey }); // DiagnosticAgent --> Agent
db.Set({ _id: 'ra' + obj.dbNodeKey, domain: domain.id, time: obj.connectTime, daid: daNodeKey }); // Agent --> DiagnosticAgent
}
break;
}
case 'query': {
// Only the diagnostic agent can do
if ((obj.agentInfo.capabilities & 0x40) != 0) {
// Return nodeid of main agent + connection status
db.Get('da' + obj.dbNodeKey, function (err, nodes) {
if ((nodes != null) && (nodes.length == 1)) {
obj.realNodeKey = nodes[0].raid;
// Get agent connection state
var agentConnected = false;
var state = parent.parent.GetConnectivityState(obj.realNodeKey);
if (state) { agentConnected = ((state.connectivity & 1) != 0) }
obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.realNodeKey, agent: agentConnected } }));
} else {
obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
}
});
}
break;
}
case 'log': {
if (((obj.agentInfo.capabilities & 0x40) != 0) && (typeof command.value.value == 'string') && (command.value.value.length < 256)) {
// If this is a diagnostic agent, log the event in the log of the main agent
var event = { etype: 'node', action: 'diagnostic', nodeid: obj.realNodeKey, snodeid: obj.dbNodeKey, domain: domain.id, msg: command.value.value };
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
}
break;
}
}
}
break;
}
case 'sysinfo': {
if ((typeof command.data == 'object') && (typeof command.data.hash == 'string')) {
// Validate command.data.
if (common.validateObjectForMongo(command.data, 1024) == false) break;
// Save to database
command.data._id = 'si' + obj.dbNodeKey;
command.data.type = 'sysinfo';
command.data.domain = domain.id;
command.data.time = Date.now();
db.Set(command.data); // Update system information in the database.
// Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
}
break;
}
case 'sysinfocheck': {
// Check system information update
db.GetHash('si' + obj.dbNodeKey, function (err, results) {
if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
});
break;
}
case 'sessions': {
// This is a list of sessions provided by the agent
if (obj.sessions == null) { obj.sessions = {}; }
if (typeof command.value != null) {
if (command.type == 'kvm') { obj.sessions.kvm = command.value; }
else if (command.type == 'terminal') { obj.sessions.terminal = command.value; }
else if (command.type == 'files') { obj.sessions.files = command.value; }
else if (command.type == 'help') { obj.sessions.help = command.value; }
else if (command.type == 'tcp') { obj.sessions.tcp = command.value; }
else if (command.type == 'udp') { obj.sessions.udp = command.value; }
else if (command.type == 'msg') { obj.sessions.msg = command.value; }
else if (command.type == 'app') { obj.sessions.app = command.value; }
}
// Any "help" session must have an associated app, if not, remove it.
if (obj.sessions.help != null) {
for (var i in obj.sessions.help) { if (obj.sessions.help[i] == null) { delete obj.sessions.help[i]; } }
if (Object.keys(obj.sessions.help).length == 0) { delete obj.sessions.help; }
}
// Inform everyone of updated sessions
obj.updateSessions();
break;
}
case 'battery': {
// Device battery and power state
if (obj.sessions == null) { obj.sessions = {}; }
if (obj.sessions.battery == null) { obj.sessions.battery = {}; }
if ((command.state == 'ac') || (command.state == 'dc')) { obj.sessions.battery.state = command.state; } else { delete obj.sessions.battery.state; }
if ((typeof command.level == 'number') && (command.level >= 0) && (command.level <= 100)) { obj.sessions.battery.level = command.level; } else { delete obj.sessions.battery.level; }
obj.updateSessions();
break;
}
case 'getcoredump': {
// Check if we requested a core dump file in the last minute, if so, ignore this.
if ((parent.lastCoreDumpRequest != null) && ((Date.now() - parent.lastCoreDumpRequest) < 60000)) break;
// Indicates if the agent has a coredump available
if ((command.exists === true) && (typeof command.agenthashhex == 'string') && (command.agenthashhex.length == 96)) {
// Check if we already have this exact dump file
const coreDumpFile = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps', obj.agentInfo.agentId + '-' + command.agenthashhex + '-' + obj.nodeid + '.dmp');
parent.fs.stat(coreDumpFile, function (err, stats) {
if (stats != null) return;
obj.coreDumpPresent = true;
// Check how many files are in the coredumps folder
const coreDumpPath = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps');
parent.fs.readdir(coreDumpPath, function (err, files) {
if ((files != null) && (files.length >= 20)) return; // Don't get more than 20 core dump files.
// Get the core dump uploaded to the server.
parent.lastCoreDumpRequest = Date.now();
obj.RequestCoreDump(command.agenthashhex, command.corehashhex);
});
});
}
break;
}
case 'tunnelCloseStats': {
// TODO: This this extra stats from the tunnel, you can merge this into the tunnel event in the database.
//console.log(command);
// Validate input
if ((command.sent == null) || (typeof command.sent != 'string')) return;
if ((command.sentActual == null) || (typeof command.sentActual != 'string')) return;
if ((command.sentActual == null) || (typeof command.sentActual != 'number')) return;
// Event the session closed compression data.
var event = { etype: 'node', action: 'sessioncompression', nodeid: obj.dbNodeKey, domain: domain.id, sent: parseInt(command.sent), sentActual: parseInt(command.sentActual), msgid: 54, msgArgs: [command.sentRatio, parseInt(command.sent), parseInt(command.sentActual)], msg: 'Agent closed session with ' + command.sentRatio + '% agent to server compression. Sent: ' + command.sent + ', Compressed: ' + command.sentActual + '.' };
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
break;
}
case 'lmsinfo': {
// Agents send the LMS port bindings
// Example: {"action":"lmsinfo","value":{"ports":["623","16992"]}}
break;
}
case 'plugin': {
if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break;
try {
parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
} catch (e) {
parent.parent.debug('agent', 'Error loading plugin handler (' + e + ')');
console.log('Error loading plugin handler (' + e + ')');
}
break;
}
case 'meshToolInfo': {
// Return information about a MeshCentral tool. Current tools are 'MeshCentralRouter' and 'MeshCentralAssistant'
// Information includes file hash and download location URL
if (typeof command.name != 'string') break;
var info = parent.parent.meshToolsBinaries[command.name];
if ((command.hash != null) && (info.hash == command.hash)) return;
// To build the connection URL, if we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
// Build the response
const responseCmd = { action: 'meshToolInfo', name: command.name, tag: command.tag, sessionid: command.sessionid, hash: info.hash, size: info.size, url: info.url };
if ((command.name == 'MeshCentralAssistant') && (command.msh == true)) { responseCmd.url = '*/' + xdomain + 'meshagents?id=10006'; } // If this is Assistant and the MSH needs to be included in the executable, change the URL.
if (command.cookie === true) { responseCmd.url += ('&auth=' + parent.parent.encodeCookie({ download: info.dlname }, parent.parent.loginCookieEncryptionKey)); }
if (command.pipe === true) { responseCmd.pipe = true; }
if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
break;
}
case 'agentupdate': {
if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
// If agent disconnection, complete and exit now.
if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
// Agent is requesting an agent update
obj.agentCoreUpdateTaskId = taskid;
const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url);
// Add the hash
if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
// Add server TLS cert hash
if (isIgnoreHashCheck() == false) {
const tlsCertHash = parent.webCertificateFullHashs[domain.id];
if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
}
// Send the agent update command
obj.send(JSON.stringify(cmd));
}
func.sessionid = command.sessionid;
// Agent update. The recovery core was loaded in the agent, send a command to update the agent
parent.parent.taskLimiter.launch(func, null, 1);
}
break;
}
case 'agentupdatedownloaded': {
if (obj.agentCoreUpdateTaskId != null) {
// Indicate this udpate task is complete
parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
delete obj.agentCoreUpdateTaskId;
}
break;
}
case 'errorlog': { // This is the agent error log
if ((!Array.isArray(command.log)) || (command.log.length == 0) || (parent.parent.agentErrorLog == null)) break;
var lastLogEntry = command.log[command.log.length - 1];
if ((lastLogEntry != null) && (typeof lastLogEntry == 'object') && (typeof lastLogEntry.t == 'number')) {
parent.fs.write(parent.parent.agentErrorLog, obj.dbNodeKey + ', ' + Date.now() + ', ' + str + '\r\n', function (err) { });
db.Set({ _id: 'al' + obj.dbNodeKey, lastEvent: lastLogEntry.t });
}
break;
}
case '2faauth': {
// Validate input
if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return;
// parse the URL
var url = null;
try { url = require('url').parse(command.url); } catch (ex) { }
if (url == null) return;
// Decode the cookie
var urlSplit = url.query.split('&c=');
if (urlSplit.length != 2) return;
const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1);
if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return;
if ((typeof authCookie.n != 'string') || (authCookie.n != obj.dbNodeKey) || (typeof authCookie.u != 'string')) return;
// Fetch the user
const user = parent.users[authCookie.u];
if (user == null) return;
// Add this device as the authentication push notification device for this user
if (authCookie.a == 'addAuth') {
// Do nothing if authentication is not approved.
// We do not want to indicate that the remote user responded to this.
if (command.approved !== true) return;
// Change the user
user.otpdev = obj.dbNodeKey;
parent.db.SetUser(user);
// Notify change
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 113, msg: "Added push notification authentication device", domain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
parent.parent.DispatchEvent(targets, obj, event);
}
// Complete 2FA checking
if (authCookie.a == 'checkAuth') {
if (typeof authCookie.s != 'string') return;
// Notify 2FA response
parent.parent.DispatchEvent(['2fadev-' + authCookie.s], obj, { etype: '2fadev', action: '2faresponse', domain: domain.id, nodeid: obj.dbNodeKey, code: authCookie.a, userid: user._id, approved: command.approved, sessionid: authCookie.s, nolog: 1 });
}
break;
}
case 'getUserImage': {
// Validate input
if (typeof command.userid != 'string') {
// Send back the default image if required
if ((command.default) || (command.sentDefault)) {
try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
obj.send(JSON.stringify(command));
}
return;
}
var useridsplit = command.userid.split('/');
if ((useridsplit.length != 3) || (useridsplit[1] != domain.id)) return;
// Add the user's real name if present
var u = parent.users[command.userid];
if (u == null) return;
if (u.name) { command.name = u.name; }
if (u.realname) { command.realname = u.realname; }
// An agent can only request images of accounts with rights to the device.
if (parent.GetNodeRights(command.userid, obj.dbMeshKey, obj.dbNodeKey) != 0) {
parent.db.Get('im' + command.userid, function (err, images) {
if ((err == null) && (images != null) && (images.length == 1)) {
// Send back the account image
command.image = images[0].image;
} else {
// Send back the default image if required
if ((command.default) || (command.sentDefault)) {
try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
}
}
obj.send(JSON.stringify(command));
});
}
break;
}
case 'getServerImage': {
if (command.agent === 'assistant') {
// Return server title and image for MeshCentral Assistant
if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
var ok = false;
if (typeof domain.assistantcustomization.title == 'string') { ok = true; command.title = domain.assistantcustomization.title; }
if (typeof domain.assistantcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
if (ok) { obj.send(JSON.stringify(command)); }
}
}
if (command.agent === 'android') {
// Return server title and image for MeshCentral Assistant
if ((domain.androidcustomization != null) && (typeof domain.androidcustomization == 'object')) {
var ok = false;
if (typeof domain.androidcustomization.title == 'string') { ok = true; command.title = domain.androidcustomization.title; }
if (typeof domain.androidcustomization.subtitle == 'string') { ok = true; command.subtitle = domain.androidcustomization.subtitle; }
if (typeof domain.androidcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.androidcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
if (ok) { obj.send(JSON.stringify(command)); }
}
}
break;
}
case 'guestShare': {
if ((command.flags == null) || (command.flags == 0)) {
// Stop any current self-share, this is allowed even if self guest sharing is not allows so to clear any old shares.
removeGuestSharing(function () {
delete obj.guestSharing;
obj.send(JSON.stringify({ action: 'guestShare', flags: command.flags, url: null, viewOnly: false }));
});
} else {
// Add a new self-share, this will replace any share for this device
if ((domain.agentselfguestsharing == null) || (domain.agentselfguestsharing == false) || (typeof command.flags != 'number')) return; // Check if agent self-sharing is allowed, this is off by default.
if ((command.flags & 2) == 0) { command.viewOnly = false; } // Only allow "view only" if desktop is shared.
addGuestSharing(command.flags, command.viewOnly, function (share) {
obj.guestSharing = true;
obj.send(JSON.stringify({ action: 'guestShare', url: share.url, flags: share.flags, viewOnly: share.viewOnly }));
})
}
break;
}
case 'amtconfig': {
// Sent by the agent when the agent needs a Intel AMT APF connection to the server
const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
break;
}
case 'script-task': {
// These command are for running regular batch jobs on the remote device
if (parent.parent.taskManager != null) { parent.parent.taskManager.agentAction(command, obj); }
break;
}
default: {
parent.agentStats.unknownAgentActionCount++;
parent.parent.debug('agent', 'Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
console.log('Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
break;
}
}
if (parent.parent.pluginHandler != null) {
parent.parent.pluginHandler.callHook('hook_processAgentData', command, obj, parent);
}
}
}
function addGuestSharing(flags, viewOnly, func) {
// Create cookie
const publicid = 'AS:' + obj.dbNodeKey;
const extrakey = getRandomAmtPassword();
const cookie = { a: 6, pid: publicid, k: extrakey }; // New style sharing cookie
const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
if (inviteCookie == null) return;
// Create the server url
var serverName = parent.getWebServerName(domain, req);
var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
// Create a device sharing database entry
var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: obj.dbNodeKey, p: flags, domain: domain.id, publicid: publicid, guestName: 'Agent', consent: 0x7F, url: url, extrakey: extrakey };
// Add expire time
if ((typeof domain.agentselfguestsharing == 'object') && (typeof domain.agentselfguestsharing.expire == 'number') && (domain.agentselfguestsharing.expire > 0)) {
shareEntry.startTime = Date.now();
shareEntry.expireTime = Date.now() + (60000 * domain.agentselfguestsharing.expire);
}
if (viewOnly === true) { shareEntry.viewOnly = true; }
parent.db.Set(shareEntry);
// Send out an event that we added a device share
var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey);
var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'addedDeviceShare', msg: 'Added device share with unlimited time', msgid: 131, msgArgs: ['Agent'], domain: domain.id };
parent.parent.DispatchEvent(targets, obj, event);
// Send device share update
parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
if (err != null) return;
// Check device sharing
var now = Date.now();
for (var i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
}
}
// Send device share update
var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
// Callback
if (func) { func({ url: url, flags: flags, viewOnly: viewOnly }); }
});
}
function removeGuestSharing(func) {
var publicid = 'AS:' + obj.dbNodeKey;
parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
if (err != null) return;
// Remove device sharing
var now = Date.now(), removedExact = null, removed = false, okDocs = [];
for (var i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc.publicid == publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
okDocs.push(doc);
}
}
// Event device share removal
if (removedExact != null) {
// Send out an event that we removed a device share
var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, ['server-shareremove']);
var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: ['Agent'], domain: domain.id, publicid: publicid };
parent.parent.DispatchEvent(targets, obj, event);
}
// If we removed any shares, send device share update
if (removed == true) {
var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
}
// Call back when done
if (func) func(removed);
});
}
// Notify update of sessions
obj.updateSessions = function () {
// Perform some clean up
for (var i in obj.sessions) { if (Object.keys(obj.sessions[i]).length == 0) { delete obj.sessions[i]; } }
if (Object.keys(obj.sessions).length == 0) { delete obj.sessions; }
// Event the new sessions, this will notify everyone that agent sessions have changed
var event = { etype: 'node', action: 'devicesessions', nodeid: obj.dbNodeKey, domain: domain.id, sessions: obj.sessions, nolog: 1 };
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
}
// Change the current core information string and event it
function ChangeAgentCoreInfo(command) {
if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
if ((command == null) || (command == null)) return; // Safety, should never happen.
// If the device is pending a change, hold.
if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentCoreInfo(command); }, 100); return; }
obj.deviceChanging = true;
// Check that the mesh exists
const mesh = parent.meshes[obj.dbMeshKey];
if (mesh == null) { delete obj.deviceChanging; return; }
// Get the node and change it if needed
db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
const device = nodes[0];
if (device.agent) {
var changes = [], change = 0, log = 0;
// Check if anything changes
if (command.name && (typeof command.name == 'string') && (command.name != device.name)) { change = 1; log = 1; device.name = command.name; changes.push('name'); }
if ((command.caps != null) && (device.agent.core != command.value)) { if ((command.value == null) && (device.agent.core != null)) { delete device.agent.core; } else { device.agent.core = command.value; } change = 1; } // Don't save this as an event to the db.
if ((command.caps != null) && ((device.agent.caps & 0xFFFFFFE7) != (command.caps & 0xFFFFFFE7))) { device.agent.caps = ((device.agent.caps & 24) + (command.caps & 0xFFFFFFE7)); change = 1; } // Allow Javascript on the agent to change all capabilities except console and javascript support, Don't save this as an event to the db.
if ((command.osdesc != null) && (typeof command.osdesc == 'string') && (device.osdesc != command.osdesc)) { device.osdesc = command.osdesc; change = 1; changes.push('os desc'); } // Don't save this as an event to the db.
if ((typeof command.root == 'boolean') && (command.root !== device.agent.root)) { change = 1; device.agent.root = command.root; }
if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
if (command.intelamt) {
if (!device.intelamt) { device.intelamt = {}; }
if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
const sku = parseInt(command.intelamt.Versions.Sku);
if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
}
}
if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {
if (device.intelamt.flags) { changes.push('AMT flags (' + device.intelamt.flags + ' --> ' + command.intelamt.Flags + ')'); } else { changes.push('AMT flags (' + command.intelamt.Flags + ')'); }
device.intelamt.flags = command.intelamt.Flags; change = 1; log = 1;
}
if ((command.intelamt.UUID != null) && (typeof command.intelamt.UUID == 'string') && (device.intelamt.uuid != command.intelamt.UUID)) { changes.push('AMT uuid'); device.intelamt.uuid = command.intelamt.UUID; change = 1; log = 1; }
}
if (command.av != null) { // Antivirus
if (!device.av) { device.av = []; }
if (JSON.stringify(device.av) != JSON.stringify(command.av)) { /*changes.push('AV status');*/ device.av = command.av; change = 1; log = 1; }
}
if (command.wsc != null) { // Windows Security Center
if (!device.wsc) { device.wsc = {}; }
if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; }
}
if (command.defender != null) { // Defender For Windows Server
if (!device.defender) { device.defender = {}; }
if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; }
}
if (command.lastbootuptime != null) { // Last Boot Up Time
if (!device.lastbootuptime) { device.lastbootuptime = ""; }
if (device.lastbootuptime != command.lastbootuptime) { /*changes.push('Last Boot Up Time');*/ device.lastbootuptime = command.lastbootuptime; change = 1; log = 1; }
}
// Push Messaging Token
if ((command.pmt != null) && (typeof command.pmt == 'string') && (device.pmt != command.pmt)) {
if (typeof device.pmt == 'string') { db.Remove('pmt_' + device.pmt); }
device.pmt = command.pmt;
change = 1; // Don't save this change as an event to the db, so no log=1.
parent.removePmtFromAllOtherNodes(device); // We need to make sure to remove this push messaging token from any other device on this server, all domains included.
}
if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
if ((mesh.mtype == 2) && (!args.wanonly)) {
// In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
// TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match.
}
// Remove old volumes and BitLocker data, this is part of sysinfo.
delete device.volumes;
// If there are changes, event the new device
if (change == 1) {
// Do some clean up if needed, these values should not be in the database.
if (device.conn != null) { delete device.conn; }
if (device.pwr != null) { delete device.pwr; }
if (device.agct != null) { delete device.agct; }
if (device.cict != null) { delete device.cict; }
// Save to the database
db.Set(device);
// Event the node change
var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
if (changes.length > 0) { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
if ((log == 0) || ((obj.agentInfo) && (obj.agentInfo.capabilities) && (obj.agentInfo.capabilities & 0x20)) || (changes.length == 0)) { event.nolog = 1; } // If this is a temporary device, don't log changes
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
}
// Device change is done.
delete obj.deviceChanging;
}
});
}
// Change the current core information string and event it
function ChangeAgentLocationInfo(command) {
if (obj.agentInfo.capabilities & 0x40) return;
if ((command == null) || (command == null)) { return; } // Safety, should never happen.
// Check that the mesh exists
const mesh = parent.meshes[obj.dbMeshKey];
if (mesh == null) return;
// If the device is pending a change, hold.
if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentLocationInfo(command); }, 100); return; }
obj.deviceChanging = true;
// Get the node and change it if needed
db.Get(obj.dbNodeKey, function (err, nodes) {
if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
const device = nodes[0];
if (device.agent) {
var changes = [], change = 0;
// Check if anything changes
if ((command.publicip) && (device.publicip != command.publicip)) { device.publicip = command.publicip; change = 1; changes.push('public ip'); }
if ((command.iploc) && (device.iploc != command.iploc)) { device.iploc = command.iploc; change = 1; changes.push('ip location'); }
// If there are changes, save and event
if (change == 1) {
// Do some clean up if needed, these values should not be in the database.
if (device.conn != null) { delete device.conn; }
if (device.pwr != null) { delete device.pwr; }
if (device.agct != null) { delete device.agct; }
if (device.cict != null) { delete device.cict; }
// Save the device
db.Set(device);
// Event the node change
var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), msgid: 59, msgArgs: [device.name, mesh.name, changes.join(', ')], msg: 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', ') };
if (obj.agentInfo.capabilities & 0x20) { event.nolog = 1; } // If this is a temporary device, don't log changes
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
}
}
// Done changing the device
delete obj.deviceChanging;
});
}
// Update the mesh agent tab in the database
function ChangeAgentTag(tag) {
if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
if ((tag != null) && (tag.length == 0)) { tag = null; }
// If the device is pending a change, hold.
if (obj.deviceChanging === true) {
var func = function ChangeAgentTagFunc() { ChangeAgentCoreInfo(ChangeAgentTagFunc.tag); }
func.tag = tag;
setTimeout(func, 100);
return;
}
obj.deviceChanging = true;
// Get the node and change it if needed
db.Get(obj.dbNodeKey, function (err, nodes) {
if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
const device = nodes[0];
if (device.agent) {
// Parse the agent tag
var agentTag = null, serverName = null, serverDesc = null, serverTags = null;
if (tag != null) {
var taglines = tag.split('\r\n').join('\n').split('\r').join('\n').split('\n');
for (var i in taglines) {
var tagline = taglines[i].trim();
if (tagline.length > 0) {
if (tagline.startsWith('~')) {
if (tagline.startsWith('~ServerName:') && (tagline.length > 12) && (serverName == null)) { serverName = tagline.substring(12).trim(); }
if (tagline.startsWith('~ServerDesc:') && (tagline.length > 12) && (serverDesc == null)) { serverDesc = tagline.substring(12).trim(); }
if (tagline.startsWith('~ServerTags:') && (tagline.length > 12) && (serverTags == null)) { serverTags = tagline.substring(12).split(','); for (var j in serverTags) { serverTags[j] = serverTags[j].trim(); } }
} else { if (agentTag == null) { agentTag = tagline; } }
}
}
}
// Set the agent tag
var changes = false;
if (device.agent.tag != agentTag) { device.agent.tag = agentTag; if ((device.agent.tag == null) || (device.agent.tag == '')) { delete device.agent.tag; } changes = true; }
if (domain.agenttag != null) {
// Set the device's server name
if ((serverName != null) && (domain.agenttag.servername === 1) && (device.name != serverName)) { device.name = serverName; changes = true; }
// Set the device's server description
if ((serverDesc != null) && (domain.agenttag.serverdesc === 1) && (device.desc != serverDesc)) { device.desc = serverDesc; changes = true; }
// Set the device's server description if there is no description
if ((serverDesc != null) && (domain.agenttag.serverdesc === 2) && (device.desc != serverDesc) && ((device.desc == null) || (device.desc == ''))) { device.desc = serverDesc; changes = true; }
if ((serverTags != null) && (domain.agenttag.servertags != null) && (domain.agenttag.servertags != 0)) {
// Sort the tags
serverTags.sort();
// Stringify the tags
var st2 = '', st1 = serverTags.join(',');
if (device.tags != null) { st2 = device.tags.join(','); }
// Set the device's server tags
if ((domain.agenttag.servertags === 1) && (st1 != st2)) { device.tags = serverTags; changes = true; }
// Set the device's server tags if there are not tags
if ((domain.agenttag.servertags === 2) && (st2 == '')) { device.tags = serverTags; changes = true; }
// Append to device's server tags
if ((domain.agenttag.servertags === 3) && (st1 != st2)) {
if (device.tags == null) { device.tags = []; }
for (var i in serverTags) { if (device.tags.indexOf(serverTags[i]) == -1) { device.tags.push(serverTags[i]); } }
device.tags.sort();
changes = true;
}
}
}
if (changes == true) {
// Do some clean up if needed, these values should not be in the database.
if (device.conn != null) { delete device.conn; }
if (device.pwr != null) { delete device.pwr; }
if (device.agct != null) { delete device.agct; }
if (device.cict != null) { delete device.cict; }
// Update the device
db.Set(device);
// Event the node change
var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), nolog: 1 };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
}
}
// Done changing the device
delete obj.deviceChanging;
});
}
// Check if we need to update this agent, return true if agent binary update required.
// Return 0 is no update needed, 1 update using native system, 2 update using meshcore system
function compareAgentBinaryHash(agentExeInfo, agentHash) {
// If this is a temporary agent and the server is set to not update temporary agents, don't update the agent.
if ((obj.agentInfo.capabilities & 0x20) && (args.temporaryagentupdate === false)) return 0;
// If we are testing the agent update system, always return true
if ((args.agentupdatetest === true) || (args.agentupdatetest === 1)) return 1;
if (args.agentupdatetest === 2) return 2;
// If the hash matches or is null, no update required.
if ((agentExeInfo.hash == agentHash) || (agentHash == '\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')) return 0;
// If this is a macOS x86 or ARM agent type and it matched the universal binary, no update required.
if ((agentExeInfo.id == 16) || (agentExeInfo.id == 29)) {
if (domain.meshAgentBinaries && domain.meshAgentBinaries[10005]) {
if (domain.meshAgentBinaries[10005].hash == agentHash) return 0;
} else {
if (parent.parent.meshAgentBinaries[10005].hash == agentHash) return 0;
}
}
// No match, update the agent.
if (args.agentupdatesystem === 2) return 2; // If set, force a meshcore update.
if (agentExeInfo.id == 3) return 2; // Due to a bug in Windows 7 SP1 environement variable exec, we always update 32bit Windows agent using MeshCore for now. Upcoming agent will have a fix for this.
// NOTE: Windows agents with no commit dates may have bad native update system, so use meshcore system instead.
// NOTE: Windows agents with commit date prior to 1612740413000 did not kill all "meshagent.exe" processes and update could fail as a result executable being locked, meshcore system will do this.
if (((obj.AgentCommitDate == null) || (obj.AgentCommitDate < 1612740413000)) && ((agentExeInfo.id == 3) || (agentExeInfo.id == 4))) return 2; // For older Windows agents, use the meshcore update technique.
return 1; // By default, use the native update technique.
}
// Request that the core dump file on this agent be uploaded to the server
obj.RequestCoreDump = function (agenthashhex, corehashhex) {
if (agenthashhex.length > 16) { agenthashhex = agenthashhex.substring(0, 16); }
const cookie = parent.parent.encodeCookie({ a: 'aft', b: 'coredump', c: obj.agentInfo.agentId + '-' + agenthashhex + '-' + obj.nodeid + '.dmp' }, parent.parent.loginCookieEncryptionKey);
obj.send('{"action":"msg","type":"tunnel","value":"*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie + '","rights":"4294967295"}');
}
// Return true if we need to ignore the agent hash check
function isIgnoreHashCheck() {
if ((args.ignoreagenthashcheck === true) || (domain.ignoreagenthashcheck === true)) return true;
// Check site wide exceptions
if (Array.isArray(args.ignoreagenthashcheck)) {
for (var i = 0; i < args.ignoreagenthashcheck.length; i++) {
if (require('ipcheck').match(obj.remoteaddr, args.ignoreagenthashcheck[i])) return true;
}
}
// Check domain wide exceptions
if (Array.isArray(domain.ignoreagenthashcheck)) {
for (var i = 0; i < domain.ignoreagenthashcheck.length; i++) {
if (require('ipcheck').match(obj.remoteaddr, domain.ignoreagenthashcheck[i])) return true;
}
}
return false;
}
// Generate a random Intel AMT password
function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
function getRandomAmtPassword() { var p; do { p = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
return obj;
};