meshcentral
Version:
Web based remote computer management server
776 lines (706 loc) • 96.3 kB
JavaScript
/**
* @description MeshCentral Intel(R) AMT MPS server
* @author Ylian Saint-Hilaire
* @copyright Intel Corporation 2018-2022
* @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 Intel AMT MPS server object
module.exports.CreateMpsServer = function (parent, db, args, certificates) {
var obj = {};
obj.fs = require('fs');
obj.path = require('path');
obj.parent = parent;
obj.db = db;
obj.args = args;
obj.certificates = certificates;
obj.ciraConnections = {}; // NodeID --> [ Socket ]
var tlsSessionStore = {}; // Store TLS session information for quick resume.
var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
const common = require('./common.js');
const net = require('net');
const tls = require('tls');
const MAX_IDLE = 90000; // 90 seconds max idle time, higher than the typical KEEP-ALIVE periode of 60 seconds
const KEEPALIVE_INTERVAL = 30; // 30 seconds is typical keepalive interval for AMT CIRA connection
// This MPS server is also a tiny HTTPS server. HTTP responses are here.
obj.httpResponses = {
'/': '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body>MeshCentral MPS server.<br />Intel® AMT computers should connect here.</body></html>'
//'/text.ico': { file: 'c:\\temp\\test.iso', maxserve: 3, maxtime: Date.now() + 15000 }
};
// Set the MPS external port only if it's not set to zero and we are not in LAN mode.
if ((args.lanonly != true) && (args.mpsport !== 0)) {
if (obj.args.mpstlsoffload) {
obj.server = net.createServer(onConnection);
} else {
if (obj.args.mpshighsecurity) {
// Higher security TLS 1.2 and 1.3 only, some older Intel AMT CIRA connections will fail.
obj.server = tls.createServer({ key: certificates.mps.key, cert: certificates.mps.cert, requestCert: true, rejectUnauthorized: false, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }, onConnection)
} else {
// Lower security MPS in order to support older Intel AMT CIRA connections, we have to turn on TLSv1.
obj.server = tls.createServer({ key: certificates.mps.key, cert: certificates.mps.cert, minVersion: 'TLSv1', requestCert: true, rejectUnauthorized: false, ciphers: "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA:@SECLEVEL=0", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION }, onConnection)
}
//obj.server.on('error', function () { console.log('MPS tls server error'); });
obj.server.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
obj.server.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
}
obj.server.listen(args.mpsport, args.mpsportbind, function () {
console.log("MeshCentral Intel(R) AMT server running on " + certificates.AmtMpsName + ":" + args.mpsport + ((args.mpsaliasport != null) ? (", alias port " + args.mpsaliasport) : "") + ".");
obj.parent.authLog('mps', 'Server listening on ' + ((args.mpsportbind != null) ? args.mpsportbind : '0.0.0.0') + ' port ' + args.mpsport + '.');
}).on("error", function (err) { console.error("ERROR: MeshCentral Intel(R) AMT server port " + args.mpsport + " is not available. Check if the MeshCentral is already running."); if (args.exactports) { process.exit(); } });
obj.server.on('tlsClientError', function (err, tlssocket) { if (args.mpsdebug) { var remoteAddress = tlssocket.remoteAddress; if (tlssocket.remoteFamily == 'IPv6') { remoteAddress = '[' + remoteAddress + ']'; } console.log('MPS:Invalid TLS connection from ' + remoteAddress + ':' + tlssocket.remotePort + '.'); } });
}
obj.parent.updateServerState('mps-port', args.mpsport);
obj.parent.updateServerState('mps-name', certificates.AmtMpsName);
if (args.mpsaliasport != null) { obj.parent.updateServerState('mps-alias-port', args.mpsaliasport); }
const APFProtocol = {
UNKNOWN: 0,
DISCONNECT: 1,
SERVICE_REQUEST: 5,
SERVICE_ACCEPT: 6,
USERAUTH_REQUEST: 50,
USERAUTH_FAILURE: 51,
USERAUTH_SUCCESS: 52,
GLOBAL_REQUEST: 80,
REQUEST_SUCCESS: 81,
REQUEST_FAILURE: 82,
CHANNEL_OPEN: 90,
CHANNEL_OPEN_CONFIRMATION: 91,
CHANNEL_OPEN_FAILURE: 92,
CHANNEL_WINDOW_ADJUST: 93,
CHANNEL_DATA: 94,
CHANNEL_CLOSE: 97,
PROTOCOLVERSION: 192,
KEEPALIVE_REQUEST: 208,
KEEPALIVE_REPLY: 209,
KEEPALIVE_OPTIONS_REQUEST: 210,
KEEPALIVE_OPTIONS_REPLY: 211,
JSON_CONTROL: 250 // This is a Mesh specific command that sends JSON to and from the MPS server.
};
/*
const APFDisconnectCode = {
HOST_NOT_ALLOWED_TO_CONNECT: 1,
PROTOCOL_ERROR: 2,
KEY_EXCHANGE_FAILED: 3,
RESERVED: 4,
MAC_ERROR: 5,
COMPRESSION_ERROR: 6,
SERVICE_NOT_AVAILABLE: 7,
PROTOCOL_VERSION_NOT_SUPPORTED: 8,
HOST_KEY_NOT_VERIFIABLE: 9,
CONNECTION_LOST: 10,
BY_APPLICATION: 11,
TOO_MANY_CONNECTIONS: 12,
AUTH_CANCELLED_BY_USER: 13,
NO_MORE_AUTH_METHODS_AVAILABLE: 14,
INVALID_CREDENTIALS: 15,
CONNECTION_TIMED_OUT: 16,
BY_POLICY: 17,
TEMPORARILY_UNAVAILABLE: 18
};
const APFChannelOpenFailCodes = {
ADMINISTRATIVELY_PROHIBITED: 1,
CONNECT_FAILED: 2,
UNKNOWN_CHANNEL_TYPE: 3,
RESOURCE_SHORTAGE: 4,
};
*/
const APFChannelOpenFailureReasonCode = {
AdministrativelyProhibited: 1,
ConnectFailed: 2,
UnknownChannelType: 3,
ResourceShortage: 4,
};
// Stat counters
var connectionCount = 0;
var userAuthRequestCount = 0;
var incorrectPasswordCount = 0;
var meshNotFoundCount = 0;
var unknownTlsNodeCount = 0;
var unknownTlsMeshIdCount = 0;
var addedTlsDeviceCount = 0;
var unknownNodeCount = 0;
var unknownMeshIdCount = 0;
var addedDeviceCount = 0;
var ciraTimeoutCount = 0;
var protocolVersionCount = 0;
var badUserNameLengthCount = 0;
var channelOpenCount = 0;
var channelOpenConfirmCount = 0;
var channelOpenFailCount = 0;
var channelCloseCount = 0;
var disconnectCommandCount = 0;
var socketClosedCount = 0;
var socketErrorCount = 0;
var maxDomainDevicesReached = 0;
// Add a CIRA connection to the connection list
function addCiraConnection(socket) {
// Check if there is already a connection of the same type
var sameType = false, connections = obj.ciraConnections[socket.tag.nodeid];
if (connections != null) { for (var i in connections) { var conn = connections[i]; if (conn.tag.connType === socket.tag.connType) { sameType = true; } } }
// Add this connection to the connections list
if (connections == null) { obj.ciraConnections[socket.tag.nodeid] = [socket]; } else { obj.ciraConnections[socket.tag.nodeid].push(socket); }
// Update connectivity state
// Report the new state of a CIRA/Relay/LMS connection after a short delay. This is to wait for the connection to have the bounded ports setup before we advertise this new connection.
socket.xxStartHold = 1;
var f = function setConnFunc() {
delete setConnFunc.socket.xxStartHold;
const ciraArray = obj.ciraConnections[setConnFunc.socket.tag.nodeid];
if ((ciraArray != null) && ((ciraArray.indexOf(setConnFunc.socket) >= 0))) { // Check if this connection is still present
if (setConnFunc.socket.tag.connType == 0) {
// Intel AMT CIRA connection. This connection indicates the remote device is present.
obj.parent.SetConnectivityState(setConnFunc.socket.tag.meshid, setConnFunc.socket.tag.nodeid, setConnFunc.socket.tag.connectTime, 2, 7, null, { name: socket.tag.name }); // 7 = Present
} else if (setConnFunc.socket.tag.connType == 1) {
// Intel AMT Relay connection. This connection does not give any information about the remote device's power state.
obj.parent.SetConnectivityState(setConnFunc.socket.tag.meshid, setConnFunc.socket.tag.nodeid, setConnFunc.socket.tag.connectTime, 8, 0, null, { name: socket.tag.name }); // 0 = Unknown
}
// Intel AMT LMS connection (connType == 2), we don't notify of these connections except telling the Intel AMT manager about them.
// If the AMT manager is present, start management of this device
if (obj.parent.amtManager != null) { obj.parent.amtManager.startAmtManagement(setConnFunc.socket.tag.nodeid, setConnFunc.socket.tag.connType, setConnFunc.socket); }
}
}
f.socket = socket;
setTimeout(f, 300);
}
// Remove a CIRA connection from the connection list
function removeCiraConnection(socket) {
// If the AMT manager is present, stop management of this device
if (obj.parent.amtManager != null) { obj.parent.amtManager.stopAmtManagement(socket.tag.nodeid, socket.tag.connType, socket); }
// Remove the connection from the list if present.
const ciraArray = obj.ciraConnections[socket.tag.nodeid];
if (ciraArray == null) return;
var i = ciraArray.indexOf(socket);
if (i == -1) return;
ciraArray.splice(i, 1);
if (ciraArray.length == 0) { delete obj.ciraConnections[socket.tag.nodeid]; } else { obj.ciraConnections[socket.tag.nodeid] = ciraArray; }
// If we are removing a connection during the hold period, don't clear any state since it was never set.
if (socket.xxStartHold == 1) return;
// Check if there is already a connection of the same type
var sameType = false, connections = obj.ciraConnections[socket.tag.nodeid];
if (connections != null) { for (var i in connections) { var conn = connections[i]; if (conn.tag.connType === socket.tag.connType) { sameType = true; } } }
if (sameType == true) return; // if there is a connection of the same type, don't change the connection state.
// Update connectivity state
if (socket.tag.connType == 0) {
obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 2, null, { name: socket.tag.name }); // CIRA
} else if (socket.tag.connType == 1) {
obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 8, null, { name: socket.tag.name }); // Relay
}
}
// Return statistics about this MPS server
obj.getStats = function () {
var ciraConnectionCount = 0;
for (var i in obj.ciraConnections) { ciraConnectionCount += obj.ciraConnections[i].length; }
return {
ciraConnections: ciraConnectionCount,
tlsSessionStore: Object.keys(tlsSessionStore).length,
connectionCount: connectionCount,
userAuthRequestCount: userAuthRequestCount,
incorrectPasswordCount: incorrectPasswordCount,
meshNotFoundCount: meshNotFoundCount,
unknownTlsNodeCount: unknownTlsNodeCount,
unknownTlsMeshIdCount: unknownTlsMeshIdCount,
addedTlsDeviceCount: addedTlsDeviceCount,
unknownNodeCount: unknownNodeCount,
unknownMeshIdCount: unknownMeshIdCount,
addedDeviceCount: addedDeviceCount,
ciraTimeoutCount: ciraTimeoutCount,
protocolVersionCount: protocolVersionCount,
badUserNameLengthCount: badUserNameLengthCount,
channelOpenCount: channelOpenCount,
channelOpenConfirmCount: channelOpenConfirmCount,
channelOpenFailCount: channelOpenFailCount,
channelCloseCount: channelCloseCount,
disconnectCommandCount: disconnectCommandCount,
socketClosedCount: socketClosedCount,
socketErrorCount: socketErrorCount,
maxDomainDevicesReached: maxDomainDevicesReached
};
}
// Required for TLS piping to MQTT broker
function SerialTunnel(options) {
var obj = new require('stream').Duplex(options);
obj.forwardwrite = null;
obj.updateBuffer = function (chunk) { this.push(chunk); };
obj._write = function (chunk, encoding, callback) { if (obj.forwardwrite != null) { obj.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); }; // Pass data written to forward
obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer()
return obj;
}
// Return's the length of an MQTT packet
function getMQTTPacketLength(chunk) {
var packet_len = 0;
if (chunk.readUInt8(0) == 16) {
if (chunk.readUInt8(1) < 128) {
packet_len += chunk.readUInt8(1) + 2;
} else {
// continuation bit, get real value and do next
packet_len += (chunk.readUInt8(1) & 0x7F) + 2;
if (chunk.readUInt8(2) < 128) {
packet_len += 1 + chunk.readUInt8(2) * 128;
} else {
packet_len += 1 + (chunk.readUInt8(2) & 0x7F) * 128;
if (chunk.readUInt8(3) < 128) {
packet_len += 1 + chunk.readUInt8(3) * 128 * 128;
} else {
packet_len += 1 + (chunk.readUInt8(3) & 0x7F) * 128 * 128;
if (chunk.readUInt8(4) < 128) {
packet_len += 1 + chunk.readUInt8(4) * 128 * 128 * 128;
} else {
packet_len += 1 + (chunk.readUInt8(4) & 0x7F) * 128 * 128 * 128;
}
}
}
}
}
return packet_len;
}
obj.onWebSocketConnection = function (socket, req) {
connectionCount++;
// connType: 0 = CIRA, 1 = Relay, 2 = LMS
socket.tag = { first: true, connType: 0, clientCert: null, accumulator: '', activetunnels: 0, boundPorts: [], websocket: true, socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0, meiState: {} };
socket.SetupChannel = function SetupChannel(targetport) { return SetupChannel.parent.SetupChannel(SetupChannel.conn, targetport); }
socket.SetupChannel.parent = obj;
socket.SetupChannel.conn = socket;
socket.websocket = 1;
socket.ControlMsg = function ControlMsg(message) { return ControlMsg.parent.SendJsonControl(ControlMsg.conn, message); }
socket.ControlMsg.parent = obj;
socket.ControlMsg.conn = socket;
socket.remoteAddr = req.clientIp;
socket.remotePort = socket._socket.remotePort;
socket._socket.bytesReadEx = 0;
socket._socket.bytesWrittenEx = 0;
parent.debug('mps', "New CIRA websocket connection");
socket.on('message', function (data) {
if (args.mpsdebug) { var buf = Buffer.from(data, 'binary'); console.log("MPS <-- (" + buf.length + "):" + buf.toString('hex')); } // Print out received bytes
// Traffic accounting
parent.webserver.trafficStats.LMSIn += (this._socket.bytesRead - this._socket.bytesReadEx);
parent.webserver.trafficStats.LMSOut += (this._socket.bytesWritten - this._socket.bytesWrittenEx);
this._socket.bytesReadEx = this._socket.bytesRead;
this._socket.bytesWrittenEx = this._socket.bytesWritten;
this.tag.accumulator += data.toString('binary'); // Append as binary string
try {
// Parse all of the APF data we can
var l = 0;
do { l = ProcessCommand(this); if (l > 0) { this.tag.accumulator = this.tag.accumulator.substring(l); } } while (l > 0);
if (l < 0) { this.terminate(); }
} catch (e) {
console.log(e);
}
});
socket.addListener('close', function () {
// Traffic accounting
parent.webserver.trafficStats.LMSIn += (this._socket.bytesRead - this._socket.bytesReadEx);
parent.webserver.trafficStats.LMSOut += (this._socket.bytesWritten - this._socket.bytesWrittenEx);
this._socket.bytesReadEx = this._socket.bytesRead;
this._socket.bytesWrittenEx = this._socket.bytesWritten;
socketClosedCount++;
parent.debug('mps', "CIRA websocket closed", this.tag.meshid, this.tag.nodeid);
removeCiraConnection(socket);
});
socket.addListener('error', function (e) {
socketErrorCount++;
parent.debug('mps', "CIRA websocket connection error", e);
});
}
// Called when a new TLS/TCP connection is accepted
function onConnection(socket) {
connectionCount++;
// connType: 0 = CIRA, 1 = Relay, 2 = LMS
if (obj.args.mpstlsoffload) {
socket.tag = { first: true, connType: 0, clientCert: null, accumulator: '', activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0, meiState: {} };
} else {
socket.tag = { first: true, connType: 0, clientCert: socket.getPeerCertificate(true), accumulator: '', activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0, meiState: {} };
}
socket.SetupChannel = function SetupChannel(targetport) { return SetupChannel.parent.SetupChannel(SetupChannel.conn, targetport); }
socket.SetupChannel.parent = obj;
socket.SetupChannel.conn = socket;
socket.ControlMsg = function ControlMsg(message) { return ControlMsg.parent.SendJsonControl(ControlMsg.conn, message); }
socket.ControlMsg.parent = obj;
socket.ControlMsg.conn = socket;
socket.bytesReadEx = 0;
socket.bytesWrittenEx = 0;
socket.remoteAddr = cleanRemoteAddr(socket.remoteAddress);
//socket.remotePort is already present, no need to set it.
socket.setEncoding('binary');
parent.debug('mps', "New CIRA connection");
// Setup the CIRA keep alive timer
socket.setTimeout(MAX_IDLE);
socket.on('timeout', () => { ciraTimeoutCount++; parent.debug('mps', "CIRA timeout, disconnecting."); obj.close(socket); });
socket.addListener('close', function () {
// Traffic accounting
parent.webserver.trafficStats.CIRAIn += (this.bytesRead - this.bytesReadEx);
parent.webserver.trafficStats.CIRAOut += (this.bytesWritten - this.bytesWrittenEx);
this.bytesReadEx = this.bytesRead;
this.bytesWrittenEx = this.bytesWritten;
socketClosedCount++;
parent.debug('mps', 'CIRA connection closed');
removeCiraConnection(socket);
});
socket.addListener('error', function (e) {
socketErrorCount++;
parent.debug('mps', 'CIRA connection error', e);
//console.log("MPS Error: " + socket.remoteAddress);
});
socket.addListener('data', function (data) {
if (args.mpsdebug) { var buf = Buffer.from(data, 'binary'); console.log("MPS <-- (" + buf.length + "):" + buf.toString('hex')); } // Print out received bytes
// Traffic accounting
parent.webserver.trafficStats.CIRAIn += (this.bytesRead - this.bytesReadEx);
parent.webserver.trafficStats.CIRAOut += (this.bytesWritten - this.bytesWrittenEx);
this.bytesReadEx = this.bytesRead;
this.bytesWrittenEx = this.bytesWritten;
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 < 5) return;
//if (!socket.tag.clientCert.subject) { console.log("MPS 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 MPS server.\r\nNo client certificate given.'); obj.close(socket); return; }
if ((socket.tag.accumulator.substring(0, 4) == 'GET ') || (socket.tag.accumulator.substring(0, 5) == 'HEAD ')) {
if (args.mpsdebug) { console.log("MPS Connection, HTTP request detected: " + socket.remoteAddress); }
socket.removeAllListeners('data');
socket.removeAllListeners('close');
socket.on('data', onHttpData);
socket.on('close', onHttpClose);
obj.httpSocket = socket;
onHttpData.call(socket, data);
return;
}
// If the MQTT broker is active, look for inbound MQTT connections
if (parent.mqttbroker != null) {
var chunk = Buffer.from(socket.tag.accumulator, 'binary');
var packet_len = 0;
if (chunk.readUInt8(0) == 16) { packet_len = getMQTTPacketLength(chunk); }
if (chunk.readUInt8(0) == 16 && (socket.tag.accumulator.length < packet_len)) return; // Minimum MQTT detection
// check if it is MQTT, need more initial packet to probe
if (chunk.readUInt8(0) == 16 && ((chunk.slice(4, 8).toString() === 'MQTT') || (chunk.slice(5, 9).toString() === 'MQTT')
|| (chunk.slice(6, 10).toString() === 'MQTT') || (chunk.slice(7, 11).toString() === 'MQTT'))) {
parent.debug('mps', "MQTT connection detected.");
socket.removeAllListeners('data');
socket.removeAllListeners('close');
socket.setNoDelay(true);
socket.serialtunnel = SerialTunnel();
socket.serialtunnel.xtransport = 'mps';
socket.serialtunnel.xip = socket.remoteAddress;
socket.on('data', function (b) { socket.serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
socket.serialtunnel.forwardwrite = function (b) { socket.write(b, 'binary') }
socket.on('close', function () { socket.serialtunnel.emit('end'); });
// Pass socket wrapper to the MQTT broker
parent.mqttbroker.handle(socket.serialtunnel);
socket.unshift(socket.tag.accumulator);
return;
}
}
socket.tag.first = false;
// Setup this node with certificate authentication
if (socket.tag.clientCert && socket.tag.clientCert.subject && socket.tag.clientCert.subject.O && socket.tag.clientCert.subject.O.length == 64) {
// This is a node where the MeshID is indicated within the CIRA certificate
var domainid = '', meshid;
var xx = socket.tag.clientCert.subject.O.split('/');
if (xx.length == 1) { meshid = xx[0]; } else { domainid = xx[0].toLowerCase(); meshid = xx[1]; }
// Check the incoming domain
var domain = obj.parent.config.domains[domainid];
if (domain == null) { console.log('CIRA connection for invalid domain. meshid: ' + meshid); obj.close(socket); return; }
socket.tag.domain = domain;
socket.tag.domainid = domainid;
socket.tag.meshid = 'mesh/' + domainid + '/' + meshid;
socket.tag.nodeid = 'node/' + domainid + '/' + require('crypto').createHash('sha384').update(common.hex2rstr(socket.tag.clientCert.modulus, 'binary')).digest('base64').replace(/\+/g, '@').replace(/\//g, '$');
socket.tag.name = socket.tag.clientCert.subject.CN;
socket.tag.connectTime = Date.now();
socket.tag.host = '';
// Fetch the node
obj.db.Get(socket.tag.nodeid, function (err, nodes) {
if ((nodes == null) || (nodes.length !== 1)) {
var mesh = obj.parent.webserver.meshes[socket.tag.meshid];
if (mesh == null) {
unknownTlsMeshIdCount++;
console.log('ERROR: Intel AMT CIRA connected with unknown groupid: ' + socket.tag.meshid);
obj.close(socket);
return;
} else if (mesh.mtype == 1) {
// 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.
maxDomainDevicesReached++;
console.log('Too many devices on this domain to accept the CIRA connection. meshid: ' + socket.tag.meshid);
obj.close(socket);
} else {
// Attempts reverse DNS loopup on the device IP address
require('dns').reverse(socket.remoteAddr, function (err, hostnames) {
var hostname = socket.remoteAddr;
if ((err == null) && (hostnames != null) && (hostnames.length > 0)) { hostname = hostnames[0]; }
// We are under the limit, create the new device.
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: domainid, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
addedTlsDeviceCount++;
var change = 'CIRA added device ' + socket.tag.name + ' to mesh ' + mesh.name;
obj.parent.DispatchEvent(['*', socket.tag.meshid], obj, { etype: 'node', action: 'addnode', node: parent.webserver.CloneSafeNode(device), msg: change, domain: domainid });
// Add the connection to the MPS connection list
addCiraConnection(socket);
});
}
});
return;
} else {
// Attempts reverse DNS loopup on the device IP address
require('dns').reverse(socket.remoteAddr, function (err, hostnames) {
var hostname = socket.remoteAddr;
if ((err == null) && (hostnames != null) && (hostnames.length > 0)) { hostname = hostnames[0]; }
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: domainid, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
addedTlsDeviceCount++;
var change = 'CIRA added device ' + socket.tag.name + ' to mesh ' + mesh.name;
obj.parent.DispatchEvent(['*', socket.tag.meshid], obj, { etype: 'node', action: 'addnode', node: parent.webserver.CloneSafeNode(device), msg: change, domain: domainid });
});
}
} else {
// New CIRA connection for unknown node, disconnect.
unknownTlsNodeCount++;
console.log('CIRA connection for unknown node with incorrect group type. meshid: ' + socket.tag.meshid);
obj.close(socket);
return;
}
} else {
// Node is already present
var node = nodes[0];
socket.tag.meshid = node.meshid; // Correct the MeshID if the node has moved.
socket.tag.name = node.name;
if ((node.intelamt != null) && (node.intelamt.state == 2)) { socket.tag.host = node.intelamt.host; }
}
// Add the connection to the MPS connection list
addCiraConnection(socket);
});
} else {
// This node connected without certificate authentication, use password auth
//console.log('Intel AMT CIRA connected without certificate authentication');
}
}
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) { obj.close(socket); }
} catch (e) {
console.log(e);
}
});
}
// Process one APF command
function ProcessCommand(socket) {
var cmd = socket.tag.accumulator.charCodeAt(0);
var len = socket.tag.accumulator.length;
var data = socket.tag.accumulator;
if (len == 0) { return 0; }
switch (cmd) {
case APFProtocol.KEEPALIVE_REQUEST: {
if (len < 5) return 0;
parent.debug('mpscmd', '--> KEEPALIVE_REQUEST');
SendKeepAliveReply(socket, common.ReadInt(data, 1));
return 5;
}
case APFProtocol.KEEPALIVE_REPLY: {
if (len < 5) return 0;
parent.debug('mpscmd', '--> KEEPALIVE_REPLY');
return 5;
}
case APFProtocol.KEEPALIVE_OPTIONS_REPLY: {
if (len < 9) return 0;
const keepaliveInterval = common.ReadInt(data, 1);
const timeout = common.ReadInt(data, 5);
parent.debug('mpscmd', '--> KEEPALIVE_OPTIONS_REPLY', keepaliveInterval, timeout);
return 9;
}
case APFProtocol.PROTOCOLVERSION: {
if (len < 93) return 0;
protocolVersionCount++;
socket.tag.MajorVersion = common.ReadInt(data, 1);
socket.tag.MinorVersion = common.ReadInt(data, 5);
socket.tag.SystemId = guidToStr(common.rstr2hex(data.substring(13, 29))).toLowerCase();
parent.debug('mpscmd', '--> PROTOCOLVERSION', socket.tag.MajorVersion, socket.tag.MinorVersion, socket.tag.SystemId);
return 93;
}
case APFProtocol.USERAUTH_REQUEST: {
if (len < 13) return 0;
userAuthRequestCount++;
var usernameLen = common.ReadInt(data, 1);
if ((usernameLen > 2048) || (len < (5 + usernameLen))) return -1;
var username = data.substring(5, 5 + usernameLen);
var serviceNameLen = common.ReadInt(data, 5 + usernameLen);
if ((serviceNameLen > 2048) || (len < (9 + usernameLen + serviceNameLen))) return -1;
var serviceName = data.substring(9 + usernameLen, 9 + usernameLen + serviceNameLen);
var methodNameLen = common.ReadInt(data, 9 + usernameLen + serviceNameLen);
if ((methodNameLen > 2048) || (len < (13 + usernameLen + serviceNameLen + methodNameLen))) return -1;
var methodName = data.substring(13 + usernameLen + serviceNameLen, 13 + usernameLen + serviceNameLen + methodNameLen);
var passwordLen = 0, password = null;
if (methodName == 'password') {
passwordLen = common.ReadInt(data, 14 + usernameLen + serviceNameLen + methodNameLen);
if ((passwordLen > 2048) || (len < (18 + usernameLen + serviceNameLen + methodNameLen + passwordLen))) return -1;
password = data.substring(18 + usernameLen + serviceNameLen + methodNameLen, 18 + usernameLen + serviceNameLen + methodNameLen + passwordLen);
}
//console.log('MPS:USERAUTH_REQUEST user=' + username + ', service=' + serviceName + ', method=' + methodName + ', password=' + password);
parent.debug('mpscmd', '--> USERAUTH_REQUEST user=' + username + ', service=' + serviceName + ', method=' + methodName + ', password=' + password);
// If the login uses a cookie, check this now
if ((username == '**MeshAgentApfTunnel**') && (password != null)) {
const cookie = parent.decodeCookie(password, parent.loginCookieEncryptionKey);
if ((cookie == null) || (cookie.a !== 'apf')) {
incorrectPasswordCount++;
socket.ControlMsg({ action: 'console', msg: 'Invalid login username/password' });
parent.debug('mps', 'Incorrect password', username, password);
SendUserAuthFail(socket);
return -1;
}
if (obj.parent.webserver.meshes[cookie.m] == null) {
meshNotFoundCount++;
socket.ControlMsg({ action: 'console', msg: 'Device group not found (1): ' + cookie.m });
parent.debug('mps', 'Device group not found (1): ' + cookie.m, username, password);
SendUserAuthFail(socket);
return -1;
}
// Setup the connection
socket.tag.nodeid = cookie.n;
socket.tag.meshid = cookie.m;
socket.tag.connectTime = Date.now();
// Add the connection to the MPS connection list
addCiraConnection(socket);
SendUserAuthSuccess(socket); // Notify the auth success on the CIRA connection
return 18 + usernameLen + serviceNameLen + methodNameLen + passwordLen;
} else {
// Check the CIRA password
if ((args.mpspass != null) && (password != args.mpspass)) {
incorrectPasswordCount++;
socket.ControlMsg({ action: 'console', msg: 'Invalid login username/password' });
parent.debug('mps', 'Incorrect password', username, password);
SendUserAuthFail(socket);
return -1;
}
// Check the CIRA username, which should be the start of the MeshID.
if (usernameLen != 16) {
badUserNameLengthCount++;
socket.ControlMsg({ action: 'console', msg: 'Username length not 16' });
parent.debug('mps', 'Username length not 16', username, password);
SendUserAuthFail(socket);
return -1;
}
// Find the initial device group for this CIRA connection. Since Intel AMT does not allow @ or $ in the username, we escape these.
// For possible for CIRA-LMS connections to still send @ or $, so we need to escape both sides.
// The initial device group will tell us what device group type and domain this connection is for
var initialMesh = null;
const meshIdStart = ('/' + username).replace(/\@/g, 'X').replace(/\$/g, 'X');
if (obj.parent.webserver.meshes) {
for (var i in obj.parent.webserver.meshes) {
if (obj.parent.webserver.meshes[i]._id.replace(/\@/g, 'X').replace(/\$/g, 'X').indexOf(meshIdStart) > 0) {
initialMesh = obj.parent.webserver.meshes[i]; break;
}
}
}
if (initialMesh == null) {
meshNotFoundCount++;
socket.ControlMsg({ action: 'console', msg: 'Device group not found (2): ' + meshIdStart + ', u: ' + username + ', p: ' + password });
parent.debug('mps', 'Device group not found (2)', meshIdStart, username, password);
SendUserAuthFail(socket);
return -1;
}
}
// If this is a agent-less mesh, use the device guid 3 times as ID.
if (initialMesh.mtype == 1) {
// Intel AMT GUID (socket.tag.SystemId) will be used as NodeID
const systemid = socket.tag.SystemId.split('-').join('');
const nodeid = Buffer.from(systemid + systemid + systemid, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
const domain = obj.parent.config.domains[initialMesh.domain];
if (domain == null) return;
socket.tag.domain = domain;
socket.tag.domainid = initialMesh.domain;
if (socket.tag.name == null) { socket.tag.name = ''; }
socket.tag.nodeid = 'node/' + initialMesh.domain + '/' + nodeid; // Turn 16bit systemid guid into 48bit nodeid that is base64 encoded
socket.tag.connectTime = Date.now();
obj.db.Get(socket.tag.nodeid, function (err, nodes) {
if ((nodes == null) || (nodes.length !== 1)) {
// 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', initialMesh.domain, function (ismax, count) {
if (ismax == true) {
// Too many devices in this domain.
maxDomainDevicesReached++;
console.log('Too many devices on this domain to accept the CIRA connection. meshid: ' + socket.tag.meshid);
obj.close(socket);
} else {
// Attempts reverse DNS loopup on the device IP address
require('dns').reverse(socket.remoteAddr, function (err, hostnames) {
var hostname = socket.remoteAddr;
if ((err == null) && (hostnames != null) && (hostnames.length > 0)) { hostname = hostnames[0]; }
// Set the device group
socket.tag.meshid = initialMesh._id;
// We are under the limit, create the new device.
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
addedDeviceCount++;
var change = 'Added CIRA device ' + socket.tag.name + ' to group ' + initialMesh.name;
obj.parent.DispatchEvent(['*', socket.tag.meshid], obj, { etype: 'node', action: 'addnode', node: parent.webserver.CloneSafeNode(device), msg: change, domain: initialMesh.domain });
// Add the connection to the MPS connection list
addCiraConnection(socket);
SendUserAuthSuccess(socket); // Notify the auth success on the CIRA connection
});
}
});
return;
} else {
// Attempts reverse DNS loopup on the device IP address
const reverseDnsLookupHandler = function (err, hostnames) {
var hostname = socket.remoteAddr;
if ((err == null) && (hostnames != null) && (hostnames.length > 0)) { hostname = hostnames[0]; }
// Set the device group
socket.tag.meshid = initialMesh._id;
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
addedDeviceCount++;
var change = 'Added CIRA device ' + socket.tag.name + ' to group ' + initialMesh.name;
obj.parent.DispatchEvent(['*', socket.tag.meshid], obj, { etype: 'node', action: 'addnode', node: parent.webserver.CloneSafeNode(device), msg: change, domain: initialMesh.domain });
}
try { require('dns').reverse(socket.remoteAddr, reverseDnsLookupHandler); } catch (ex) { reverseDnsLookupHandler(ex, null); }
}
} else {
// Node is already present
var node = nodes[0];
socket.tag.meshid = node.meshid;
socket.tag.name = node.name;
if ((node.intelamt != null) && (node.intelamt.state == 2)) { socket.tag.host = node.intelamt.host; }
}
// Add the connection to the MPS connection list
addCiraConnection(socket);
SendUserAuthSuccess(socket); // Notify the auth success on the CIRA connection
});
} else if (initialMesh.mtype == 2) { // If this is a agent mesh, search the mesh for this device UUID
// Intel AMT GUID (socket.tag.SystemId) will be used to search the node
obj.db.getAmtUuidMeshNode(initialMesh.domain, initialMesh.mtype, socket.tag.SystemId, function (err, nodes) { // TODO: Need to optimize this request with indexes
if ((nodes == null) || (nodes.length === 0) || (obj.parent.webserver.meshes == null)) {
// New CIRA connection for unknown node, create a new device.
unknownNodeCount++;
co