mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-12-23 22:13:14 +03:00
378 lines
23 KiB
JavaScript
378 lines
23 KiB
JavaScript
/**
|
|
* @description MeshCentral v1 legacy Swarm Server, used to update agents and get them on MeshCentral2
|
|
* @author Ylian Saint-Hilaire
|
|
* @copyright Intel Corporation 2018-2019
|
|
* @license Apache-2.0
|
|
* @version v0.0.1
|
|
*/
|
|
|
|
/*jslint node: true */
|
|
/*jshint node: true */
|
|
/*jshint strict:false */
|
|
/*jshint -W097 */
|
|
/*jshint esversion: 6 */
|
|
"use strict";
|
|
|
|
// Construct a legacy Swarm Server server object
|
|
module.exports.CreateSwarmServer = function (parent, db, args, certificates) {
|
|
var obj = {};
|
|
obj.parent = parent;
|
|
obj.db = db;
|
|
obj.args = args;
|
|
obj.certificates = certificates;
|
|
obj.legacyAgentConnections = {};
|
|
obj.migrationAgents = {};
|
|
const common = require('./common.js');
|
|
//const net = require('net');
|
|
const tls = require('tls');
|
|
const forge = require('node-forge');
|
|
|
|
const LegacyMeshProtocol = {
|
|
NODEPUSH: 1, // Used to send a node block to another peer.
|
|
NODEPULL: 2, // Used to send a pull block to another peer.
|
|
NODENOTIFY: 3, // Used to indicate the node ID to other peers.
|
|
NODECHALLENGE: 4, // Used to challenge a node identity.
|
|
NODECRESPONSE: 5, // Used to respond to a node challenge.
|
|
TARGETSTATUS: 6, // Used to send the peer connection status list.
|
|
LOCALEVENT: 7, // Used to send local events to subscribers.
|
|
AESCRYPTO: 8, // Used to send an encrypted block of data.
|
|
SESSIONKEY: 9, // Used to send a session key to a remote node.
|
|
SYNCSTART: 10, // Used to send kick off the SYNC request, send the start NodeID.
|
|
SYNCMETADATA: 11, // Used to send a sequence of NodeID & serial numbers.
|
|
SYNCREQUEST: 12, // Used to send a sequence of NodeID's to request.
|
|
NODEID: 13, // Used to send the NodeID in the clear. Used for multicast.
|
|
AGENTID: 14, // Used to send the AgentID & version to the other node.
|
|
PING: 15, // Used to query a target for the presence of the mesh agent (PB_NODEID response expected).
|
|
SETUPADMIN: 16, // Used to set the trusted mesh identifier, this code can only be used from local settings file.
|
|
POLICY: 17, // Used to send a policy block to another peer.
|
|
POLICYSECRET: 18, // Used to encode the PKCS12 private key of a policy block.
|
|
EVENTMASK: 19, // Used by the mesh service to change the event mask.
|
|
RECONNECT: 20, // Used by the mesh service to indicate disconnect & reconnection after n seconds.
|
|
GETSTATE: 21, // Used by the mesh service to obtain agent state.
|
|
CERTENCRYPTED: 22, // Used to send a certificate encrypted message to a node.
|
|
GETCOOKIE: 23, // Used to request a certificate encryption anti-replay cookie.
|
|
COOKIE: 24, // Used to carry an anti-replay cookie to a requestor.
|
|
SESSIONCKEY: 25, // Used to send a session key to a remote console.
|
|
INTERFACE: 26, // Used to send a local interface blob to a management console.
|
|
MULTICAST: 27, // Used by the mesh service to cause the agent to send a multicast.
|
|
SELFEXE: 28, // Used to transfer our own agent executable.
|
|
LEADERBADGE: 29, // User to send a leadership badge.
|
|
NODEINFO: 30, // Used to indicate a block information update to the web service.
|
|
TARGETEVENT: 31, // Used to send a single target update event.
|
|
DEBUG: 33, // Used to send debug information to web service.
|
|
TCPRELAY: 34, // Used to operate mesh leader TCP relay sockets
|
|
CERTSIGNED: 35, // Used to send a certificate signed message to a node.
|
|
ERRORCODE: 36, // Used to notify of an error.
|
|
MESSAGE: 37, // Used to route messages between nodes.
|
|
CMESSAGE: 38, // Used to embed a interface identifier along with a PB_MESSAGE.
|
|
EMESSAGE: 39, // Used to embed a target encryption certificate along with a MESSAGE or CMESSAGE.
|
|
SEARCH: 40, // Used to send a custom search to one or more remote nodes.
|
|
MESSAGERELAY: 41, // Used by no-certificate consoles to send hopping messages to nodes.
|
|
USERINPUT: 42, // Used to send user keyboard input to a target computer
|
|
APPID: 43, // Used to send a block of data to a specific application identifier.
|
|
APPSUBSCRIBE: 44, // Used to perform local app subscription to an agent.
|
|
APPDIRECT: 45, // Used to send message directly to remote applications.
|
|
APPREQACK: 46, // Used to request an ack message.
|
|
APPACK: 47, // Used to ack a received message.
|
|
SERVERECHO: 48, // Server will echo this message, used for testing.
|
|
KVMINFO: 49, // Used to send local KVM slave process information to mesh agent.
|
|
REMOTEWAKE: 50, // Used to send remote wake information to server.
|
|
NEWCONNECTTOKEN: 51, // Used to send a new connection token to the Swarm Server.
|
|
WIFISCAN: 52, // Used to send visible WIFI AP's to the server.
|
|
AMTPROVISIONING: 53, // Used by the agent to send Intel AMT provisioning information to the server.
|
|
ANDROIDCOMMAND: 54, // Send a Android OS specific command (Android only).
|
|
NODEAPPDATA: 55, // Used to send application specific data block to the server for storage.
|
|
PROXY: 56, // Used to indicate the currently used proxy setting string.
|
|
FILEOPERATION: 57, // Used to perform short file operations.
|
|
APPSUBSCRIBERS: 58, // Used request and send to the mesh server the list of subscribed applications
|
|
CUSTOM: 100, // Message containing application specific data.
|
|
USERAUTH: 1000, // Authenticate a user to the swarm server.
|
|
USERMESH: 1001, // Request or return the mesh list for this console.
|
|
USERMESHS: 1002, // Send mesh overview information to the console.
|
|
USERNODES: 1003, // Send node overview information to the console.
|
|
JUSERMESHS: 1004, // Send mesh overview information to the console in JSON format.
|
|
JUSERNODES: 1005, // Send node overview information to the console in JSON format.
|
|
USERPOWERSTATE: 1006, // Used to send a power command from the console to the server.
|
|
JMESHPOWERTIMELINE: 1007, // Send the power timeline for all nodes in a mesh.
|
|
JMESHPOWERSUMMARY: 1008, // Send the power summary for sum of all nodes in a mesh.
|
|
USERCOMMAND: 1009, // Send a user admin text command to and from the server.
|
|
POWERBLOCK: 1010, // Request/Response of block of power state information.
|
|
MESHACCESSCHANGE: 1011, // Notify a console of a change in accessible meshes.
|
|
COOKIEAUTH: 1012, // Authenticate a user using a crypto cookie.
|
|
NODESTATECHANGE: 1013, // Indicates a node has changed power state.
|
|
JUSERNODE: 1014, // Send node overview information to the console in JSON format.
|
|
AMTWSMANEVENT: 1015, // Intel AMT WSMAN event sent to consoles.
|
|
ROUTINGCOOKIE: 1016, // Used by a console to request a routing cookie.
|
|
JCOLLABORATION: 1017, // Request/send back JSON collaboration state.
|
|
JRELATIONS: 1018, // Request/send back JSON relations state.
|
|
SETCOLLABSTATE: 1019, // Set the collaboration state for this session.
|
|
ADDRELATION: 1020, // Request that a new relation be added.
|
|
DELETERELATION: 1021, // Request a relation be deleted.
|
|
ACCEPTRELATION: 1022, // Request relation invitation be accepted.
|
|
RELATIONCHANGEEVENT: 1023, // Notify that a relation has changed.
|
|
COLLBCHANGEEVENT: 1024, // Notify that a collaboration state has change.
|
|
MULTICONSOLEMESSAGE: 1025, // Send a message to one or more console id's.
|
|
CONSOLEID: 1026, // Notify a console of it's console id.
|
|
CHANGERELATIONDATA: 1027, // Request that relation data be changed.
|
|
SETUSERDATA: 1028, // Set user data
|
|
GETUSERDATA: 1029, // Get user data
|
|
SERVERAUTH: 1030, // Used to verify the certificate of the server
|
|
USERAUTH2: 1031, // Authenticate a user to the swarm server (Uses SHA1 SALT)
|
|
GUESTREMOTEDESKTOP: 2001, // Guest usage: Remote Desktop
|
|
GUESTWEBRTCMESH: 2002 // Guest usage: WebRTC Mesh
|
|
};
|
|
|
|
obj.server = tls.createServer({ key: certificates.swarmserver.key, cert: certificates.swarmserver.cert, requestCert: true, rejectUnauthorized: false }, onConnection);
|
|
obj.server.listen(args.swarmport, function () { console.log('MeshCentral Legacy Swarm Server running on ' + certificates.CommonName + ':' + args.swarmport + '.'); obj.parent.updateServerState('swarm-port', args.swarmport); }).on('error', function (err) { console.error('ERROR: MeshCentral Swarm Server server port ' + args.swarmport + ' is not available.'); if (args.exactports) { process.exit(); } });
|
|
loadMigrationAgents();
|
|
|
|
// Load all migration agents along with full executable in memory
|
|
function loadMigrationAgents() {
|
|
var migrationAgentsDir = null, migrationAgentsPath = obj.parent.path.join(obj.parent.datapath, 'migrationagents');
|
|
try { migrationAgentsDir = obj.parent.fs.readdirSync(migrationAgentsPath); } catch (e) { }
|
|
if (migrationAgentsDir != null) {
|
|
for (var i in migrationAgentsDir) {
|
|
if (migrationAgentsDir[i].toLowerCase().startsWith('meshagent-')) {
|
|
var migrationAgentName = obj.parent.path.join(migrationAgentsPath, migrationAgentsDir[i]);
|
|
var agentInfo = migrationAgentsDir[i].substring(10).split('.');
|
|
var agentVersion = parseInt(agentInfo[0]);
|
|
var agentArch = parseInt(agentInfo[1]);
|
|
var agentBinary = obj.parent.fs.readFileSync(migrationAgentName);
|
|
if (obj.migrationAgents[agentArch] == null) { obj.migrationAgents[agentArch] = {}; }
|
|
if (obj.migrationAgents[agentArch][agentVersion] == null) { obj.migrationAgents[agentArch][agentVersion] = { arch: agentArch, ver: agentVersion, path: migrationAgentName, binary: agentBinary }; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called when a legacy agent connects to this server
|
|
function onConnection(socket) {
|
|
// Check for blocked IP address
|
|
if (checkSwarmIpAddress(socket, obj.args.swarmallowedip) == false) { Debug(1, "SWARM:New blocked agent connection"); return; }
|
|
|
|
socket.tag = { first: true, clientCert: socket.getPeerCertificate(true), accumulator: "", socket: socket };
|
|
socket.setEncoding('binary');
|
|
socket.pingTimer = setInterval(function () { obj.SendCommand(socket, LegacyMeshProtocol.PING); }, 20000);
|
|
Debug(1, 'SWARM:New legacy agent connection');
|
|
|
|
socket.addListener("data", function (data) {
|
|
if (args.swarmdebug) { var buf = Buffer.from(data, "binary"); console.log('SWARM <-- (' + buf.length + '):' + buf.toString('hex')); } // Print out received bytes
|
|
socket.tag.accumulator += data;
|
|
|
|
// Detect if this is an HTTPS request, if it is, return a simple answer and disconnect. This is useful for debugging access to the MPS port.
|
|
if (socket.tag.first == true) {
|
|
if (socket.tag.accumulator.length < 3) return;
|
|
if (socket.tag.accumulator.substring(0, 3) == 'GET') { /*console.log("Swarm Connection, HTTP GET detected: " + socket.remoteAddress);*/ socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>MeshCentral2 legacy swarm server.<br />MeshCentral1 mesh agents should connect here for updates.</body></html>'); socket.end(); return; }
|
|
socket.tag.first = false;
|
|
}
|
|
|
|
// A client certificate is required
|
|
if (!socket.tag.clientCert.subject) { /*console.log("Swarm Connection, no client cert: " + socket.remoteAddress);*/ socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nMeshCentral2 legacy swarm server.\r\nNo client certificate given.'); socket.end(); return; }
|
|
|
|
try {
|
|
// Parse all of the APF data we can
|
|
var l = 0;
|
|
do { l = ProcessCommand(socket); if (l > 0) { socket.tag.accumulator = socket.tag.accumulator.substring(l); } } while (l > 0);
|
|
if (l < 0) { socket.end(); }
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
});
|
|
|
|
// Process one AFP command
|
|
function ProcessCommand(socket) {
|
|
if (socket.tag.accumulator.length < 4) return 0;
|
|
var cmd = common.ReadShort(socket.tag.accumulator, 0);
|
|
var len = common.ReadShort(socket.tag.accumulator, 2);
|
|
if (len > socket.tag.accumulator.length) return 0;
|
|
var data = socket.tag.accumulator.substring(4, len);
|
|
//console.log('Swarm: Cmd=' + cmd + ', Len=' + len + '.');
|
|
|
|
switch (cmd) {
|
|
case LegacyMeshProtocol.NODEPUSH: {
|
|
Debug(3, 'Swarm:NODEPUSH');
|
|
var nodeblock = obj.decodeNodeBlock(data);
|
|
if ((nodeblock != null) && (nodeblock.agenttype != null) && (nodeblock.agentversion != null)) {
|
|
Debug(3, 'Swarm:NODEPUSH:' + JSON.stringify(nodeblock));
|
|
|
|
// Figure out what is the next agent version we need.
|
|
var nextAgentVersion = 0;
|
|
if (nodeblock.agentversion < 200) { nextAgentVersion = 200; } // If less then 200, move to transitional MC1 agent.
|
|
if (nodeblock.agentversion == 200) { nextAgentVersion = 201; } // If at 200, move to first MC2 agent.
|
|
|
|
// See if we need to start the agent update
|
|
if ((nextAgentVersion > 0) && (obj.migrationAgents[nodeblock.agenttype] != null) && (obj.migrationAgents[nodeblock.agenttype][nextAgentVersion] != null)) {
|
|
// Start the update
|
|
socket.tag.update = obj.migrationAgents[nodeblock.agenttype][nextAgentVersion];
|
|
socket.tag.updatePtr = 0;
|
|
console.log('Performing legacy agent update from ' + nodeblock.agentversion + '.' + nodeblock.agenttype + ' to ' + socket.tag.update.ver + '.' + socket.tag.update.arch + ' on ' + nodeblock.agentname + '.');
|
|
obj.SendCommand(socket, LegacyMeshProtocol.GETSTATE, common.IntToStr(5) + common.IntToStr(0)); // agent.SendQuery(5, 0); // Start the agent download
|
|
} else {
|
|
console.log('No legacy agent update for ' + nodeblock.agentversion + '.' + nodeblock.agenttype + ' on ' + nodeblock.agentname + '.');
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case LegacyMeshProtocol.AMTPROVISIONING: {
|
|
Debug(3, 'Swarm:AMTPROVISIONING');
|
|
obj.SendCommand(socket, LegacyMeshProtocol.AMTPROVISIONING, common.ShortToStr(1));
|
|
break;
|
|
}
|
|
case LegacyMeshProtocol.GETSTATE: {
|
|
Debug(3, 'Swarm:GETSTATE');
|
|
if (len < 12) break;
|
|
var statecmd = common.ReadInt(data, 0);
|
|
//var statesync = common.ReadInt(data, 4);
|
|
switch (statecmd) {
|
|
case 6: { // Ask for agent block
|
|
if (socket.tag.update != null) {
|
|
// Send an agent block
|
|
var l = Math.min(socket.tag.update.binary.length - socket.tag.updatePtr, 16384);
|
|
obj.SendCommand(socket, LegacyMeshProtocol.GETSTATE, common.IntToStr(6) + common.IntToStr(socket.tag.updatePtr) + socket.tag.update.binary.toString('binary', socket.tag.updatePtr, socket.tag.updatePtr + l)); // agent.SendQuery(6, AgentFileLen + AgentBlock);
|
|
Debug(3, 'Swarm:Sending agent block, ptr = ' + socket.tag.updatePtr + ', len = ' + l);
|
|
|
|
socket.tag.updatePtr += l;
|
|
if (socket.tag.updatePtr >= socket.tag.update.binary.length) {
|
|
// Send end-of-transfer
|
|
obj.SendCommand(socket, LegacyMeshProtocol.GETSTATE, common.IntToStr(7) + common.IntToStr(socket.tag.update.binary.length)); //agent.SendQuery(7, AgentFileLen);
|
|
Debug(3, 'Swarm:Sending end of agent, ptr = ' + socket.tag.updatePtr);
|
|
delete socket.tag.update;
|
|
delete socket.tag.updatePtr;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
// All other state commands from the legacy agent must be ignored.
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case LegacyMeshProtocol.APPSUBSCRIBERS: {
|
|
Debug(3, 'Swarm:APPSUBSCRIBERS');
|
|
break;
|
|
}
|
|
default: {
|
|
Debug(1, 'Swarm:Unknown command: ' + cmd + ' of len ' + len + '.');
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
socket.addListener("close", function () {
|
|
Debug(1, 'Swarm:Connection closed');
|
|
try { delete obj.ciraConnections[socket.tag.nodeid]; } catch (e) { }
|
|
obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 2);
|
|
if (socket.pingTimer != null) { clearInterval(socket.pingTimer); delete socket.pingTimer; }
|
|
});
|
|
|
|
socket.addListener("error", function () {
|
|
//console.log("Swarm Error: " + socket.remoteAddress);
|
|
});
|
|
}
|
|
|
|
function getTagClass(data, tagClass, type) {
|
|
if ((data == null) || (data.value == null)) return;
|
|
for (var i in data.value) {
|
|
//console.log(JSON.stringify(data.value[i]));
|
|
if ((data.value[i].tagClass == tagClass) && (data.value[i].type == type)) {
|
|
return data.value[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode a node push block
|
|
obj.decodeNodeBlock = function (data) {
|
|
try {
|
|
// Traverse the DER to get the raw data (Not sure if this works all the time)
|
|
var info = {}, ptr = 68, der = forge.asn1.fromDer(forge.util.createBuffer(data, 'binary'));
|
|
der = getTagClass(der, 128, 0);
|
|
der = getTagClass(der, 0, 16);
|
|
der = getTagClass(der, 0, 16);
|
|
der = getTagClass(der, 128, 0);
|
|
der = getTagClass(der, 0, 4);
|
|
var binarydata = der.value;
|
|
|
|
// Get the basic header values
|
|
info.certhashhex = common.rstr2hex(binarydata.substring(0, 32)); // Hash of the complete mesh agent certificate
|
|
info.nodeidhex = common.rstr2hex(binarydata.substring(32, 64)); // Old mesh agent nodeid
|
|
info.serialNumber = common.ReadIntX(binarydata, 64); // Block serial number
|
|
|
|
// Got thru the sub-blocks
|
|
while (ptr < binarydata.length) {
|
|
var btyp = common.ReadShort(binarydata, ptr), blen = common.ReadShort(binarydata, ptr + 2), bdata = binarydata.substring(ptr + 4, ptr + 4 + blen);
|
|
switch (btyp) {
|
|
case 1: { // PBST_COMPUTERINFO
|
|
info.agenttype = common.ReadShortX(bdata, 0);
|
|
info.agentbuild = common.ReadShortX(bdata, 2);
|
|
info.agentversion = common.ReadIntX(bdata, 4);
|
|
info.agentname = bdata.substring(8, 64 + 8);
|
|
var xx = info.agentname.indexOf('\u0000');
|
|
if (xx >= 0) { info.agentname = info.agentname.substring(0, xx); }
|
|
info.agentosdesc = bdata.substring(64 + 8, 64 + 64 + 8);
|
|
xx = info.agentosdesc.indexOf('\u0000');
|
|
if (xx >= 0) { info.agentosdesc = info.agentosdesc.substring(0, xx); }
|
|
return info;
|
|
}
|
|
default: {
|
|
// All other commands from the legacy agent must be ignored.
|
|
break;
|
|
}
|
|
}
|
|
ptr += blen;
|
|
}
|
|
return info;
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Disconnect legacy agent connection
|
|
obj.close = function (socket) {
|
|
try { socket.close(); } catch (e) { }
|
|
try { delete obj.ciraConnections[socket.tag.nodeid]; } catch (e) { }
|
|
obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 2);
|
|
};
|
|
|
|
obj.SendCommand = function (socket, cmdid, data) {
|
|
if (data == null) { data = ''; }
|
|
Write(socket, common.ShortToStr(cmdid) + common.ShortToStr(data.length + 4) + data);
|
|
};
|
|
|
|
function Write(socket, data) {
|
|
if (args.swarmdebug) {
|
|
// Print out sent bytes
|
|
var buf = Buffer.from(data, "binary");
|
|
console.log('SWARM --> (' + buf.length + '):' + buf.toString('hex'));
|
|
socket.write(buf);
|
|
} else {
|
|
socket.write(Buffer.from(data, "binary"));
|
|
}
|
|
}
|
|
|
|
// Check if the source IP address is allowed for a given allowed list, return false if not
|
|
function checkSwarmIpAddress(socket, allowedIpList) {
|
|
if (allowedIpList == null) { return true; }
|
|
try {
|
|
var ip = socket.remoteAddress;
|
|
if (ip) { for (var i = 0; i < allowedIpList.length; i++) { if (require('ipcheck').match(ip, allowedIpList[i])) { return true; } } }
|
|
} catch (e) { console.log(e); }
|
|
return false;
|
|
}
|
|
|
|
// Debug
|
|
function Debug(lvl) {
|
|
if (lvl > obj.parent.debugLevel) return;
|
|
if (arguments.length == 2) { console.log(arguments[1]); }
|
|
else if (arguments.length == 3) { console.log(arguments[1], arguments[2]); }
|
|
else if (arguments.length == 4) { console.log(arguments[1], arguments[2], arguments[3]); }
|
|
else if (arguments.length == 5) { console.log(arguments[1], arguments[2], arguments[3], arguments[4]); }
|
|
else if (arguments.length == 6) { console.log(arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]); }
|
|
else if (arguments.length == 7) { console.log(arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6]); }
|
|
}
|
|
|
|
return obj;
|
|
};
|