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