Early work on IP KVM integration.

Ylian Saint-Hilaire committed Dec 2, 2021 at 20:20 UTC 8f36513078ec0d591aae084c478b738d63d96dd7
5 files changed +353 -5
MeshCentralServer.njsproj
+1
@@ -109,6 +109,7 @@
109 <Compile Include="meshbot.js" />
110 <Compile Include="meshctrl.js" />
111 <Compile Include="meshdesktopmultiplex.js" />
112 + <Compile Include="meshipkvm.js" />
113 <Compile Include="meshmail.js" />
114 <Compile Include="meshrelay.js" />
115 <Compile Include="meshsms.js" />
meshcentral.js
+10 -1
@@ -59,6 +59,7 @@ function CreateMeshCentralServer(config, args) {
59 obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
60 obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
61 obj.multiServer = null;
62 + obj.ipKvmManager = null;
63 obj.maintenanceTimer = null;
64 obj.serverId = null;
65 obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
@@ -1566,7 +1567,7 @@ function CreateMeshCentralServer(config, args) {
1567 if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
1568
1569 // Start the web server and if needed, the redirection web server.
1569 - obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates);
1570 + obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
1571 if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
1572
1573 // Update proxy certificates
@@ -1815,6 +1816,14 @@ function CreateMeshCentralServer(config, args) {
1816 });
1817 };
1818
1819 + // Called when the web server finished loading
1820 + obj.StartEx5 = function () {
1821 + // Setup the email server for each domain
1822 + var ipKvmSupport = false;
1823 + for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
1824 + if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
1825 + }
1826 +
1827 // Refresh any certificate hashs from the reverse proxy
1828 obj.pendingProxyCertificatesRequests = 0;
1829 obj.lastProxyCertificatesRequest = null;
meshipkvm.js new
+335
@@ -0,0 +1,335 @@
1 +/**
2 +* @description MeshCentral IP KVM Management Module
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2021
5 +* @license Apache-2.0
6 +* @version v0.0.1
7 +*/
8 +
9 +function CreateIPKVMManager(parent) {
10 + const obj = {};
11 + const managedGroups = {} // meshid --> Manager
12 +
13 + // Subscribe for mesh creation events
14 + parent.AddEventDispatch(['server-createmesh', 'server-deletemesh'], obj);
15 + obj.HandleEvent = function (source, event, ids, id) {
16 + if ((event != null) && (event.action == 'createmesh') && (event.mtype == 4)) {
17 + // Start managing this new device group
18 + startManagement(parent.webserver.meshes[event.meshid]);
19 + } else if ((event != null) && (event.action == 'deletemesh') && (event.mtype == 4)) {
20 + // Stop managing this device group
21 + stopManagement(event.meshid);
22 + }
23 + }
24 +
25 + // Run thru the list of device groups that require
26 + for (var i in parent.webserver.meshes) {
27 + const mesh = parent.webserver.meshes[i];
28 + if ((mesh.mtype == 4) && (mesh.deleted == null)) { startManagement(mesh); }
29 + }
30 +
31 + // Start managing a IP KVM device
32 + function startManagement(mesh) {
33 + if ((mesh == null) || (mesh.mtype != 4) || (mesh.kvm == null) || (mesh.deleted != null) || (managedGroups[mesh._id] != null)) return;
34 + var port = 443, hostSplit = mesh.kvm.host.split(':'), host = hostSplit[0];
35 + if (hostSplit.length == 2) { port = parseInt(hostSplit[1]); }
36 + if (mesh.kvm.model == 1) { // Raritan KX III
37 + const manager = CreateRaritanKX3Manager(host, port, mesh.kvm.user, mesh.kvm.pass);
38 + manager.meshid = mesh._id;
39 + managedGroups[mesh._id] = manager;
40 + manager.onStateChanged = onStateChanged;
41 + manager.onPortsChanged = onPortsChanged;
42 + manager.start();
43 + }
44 + }
45 +
46 + // Stop managing a IP KVM device
47 + function stopManagement(meshid) {
48 + const manager = managedGroups[meshid];
49 + if (manager != null) { delete managedGroups[meshid]; manager.stop(); }
50 + }
51 +
52 + // Called when a KVM device changes state
53 + function onStateChanged(sender, state) {
54 + console.log('State: ' + ['Disconnected', 'Connecting', 'Connected'][state]);
55 + if (state == 2) {
56 + console.log('DeviceModel:', sender.deviceModel);
57 + console.log('FirmwareVersion:', sender.firmwareVersion);
58 + }
59 + }
60 +
61 + // Called when a KVM device changes state
62 + function onPortsChanged(sender, updatedPorts) {
63 + for (var i = 0; i < updatedPorts.length; i++) {
64 + const port = sender.ports[updatedPorts[i]];
65 + if ((port.Status == 1) && (port.Class == 'KVM')) {
66 + console.log(port.PortNumber + ', ' + port.PortId + ', ' + port.Name + ', ' + port.Type + ', ' + ((port.StatAvailable == 0) ? 'Idle' : 'Connected'));
67 + }
68 + }
69 + }
70 +
71 + return obj;
72 +}
73 +
74 +function CreateRaritanKX3Manager(hostname, port, username, password) {
75 + const https = require('https');
76 + const obj = {};
77 + var updateTimer = null;
78 + var retryTimer = null;
79 +
80 + obj.authCookie = null;
81 + obj.state = 0; // 0 = Disconnected, 1 = Connecting, 2 = Connected
82 + obj.ports = [];
83 + obj.portCount = 0;
84 + obj.portHash = null;
85 + obj.deviceCount = 0;
86 + obj.deviceHash = null;
87 + obj.started = false;
88 +
89 + // Events
90 + obj.onStateChanged = null;
91 + obj.onPortsChanged = null;
92 +
93 + function onCheckServerIdentity(cert) {
94 + console.log('TODO: Certificate Check');
95 + }
96 +
97 + obj.start = function () {
98 + if (obj.started) return;
99 + obj.started = true;
100 + if (obj.state == 0) connect();
101 + }
102 +
103 + obj.stop = function () {
104 + if (!obj.started) return;
105 + obj.started = false;
106 + if (retryTimer != null) { clearTimeout(retryTimer); retryTimer = null; }
107 + setState(0);
108 + }
109 +
110 + function setState(newState) {
111 + if (obj.state == newState) return;
112 + obj.state = newState;
113 + if (obj.onStateChanged != null) { obj.onStateChanged(obj, newState); }
114 + if ((newState == 2) && (updateTimer == null)) { updateTimer = setInterval(obj.update, 10000); }
115 + if ((newState != 2) && (updateTimer != null)) { clearInterval(updateTimer); updateTimer = null; }
116 + if ((newState == 0) && (obj.started == true) && (retryTimer == null)) { retryTimer = setTimeout(connect, 20000); }
117 + }
118 +
119 + function connect() {
120 + if (obj.state != 0) return;
121 + setState(1); // 1 = Connecting
122 + obj.authCookie = null;
123 + if (retryTimer != null) { clearTimeout(retryTimer); retryTimer = null; }
124 + const data = new TextEncoder().encode('is_dotnet=0&is_javafree=0&is_standalone_client=0&is_javascript_kvm_client=1&is_javascript_rsc_client=1&login=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&action_login=Login');
125 + const options = {
126 + hostname: hostname,
127 + port: port,
128 + rejectUnauthorized: false,
129 + checkServerIdentity: onCheckServerIdentity,
130 + path: '/auth.asp?client=javascript', // ?client=standalone
131 + method: 'POST',
132 + headers: {
133 + 'Content-Type': 'text/html; charset=UTF-8',
134 + 'Content-Length': data.length
135 + }
136 + }
137 + const req = https.request(options, function (res) {
138 + if (obj.state == 0) return;
139 + if ((res.statusCode != 302) || (res.headers['set-cookie'] == null) || (res.headers['location'] == null)) { setState(0); return; }
140 + for (var i in res.headers['set-cookie']) { if (res.headers['set-cookie'][i].startsWith('pp_session_id=')) { obj.authCookie = res.headers['set-cookie'][i].substring(14).split(';')[0]; } }
141 + if (obj.authCookie == null) { setState(0); return; }
142 + res.on('data', function (d) { })
143 + fetchInitialInformation();
144 + })
145 + req.on('error', function (error) { setState(0); })
146 + req.write(data);
147 + req.end();
148 + }
149 +
150 + function checkCookie() {
151 + if (obj.state != 2) return;
152 + const options = {
153 + hostname: hostname,
154 + port: port,
155 + rejectUnauthorized: false,
156 + checkServerIdentity: onCheckServerIdentity,
157 + path: '/cookiecheck.asp',
158 + method: 'GET',
159 + headers: {
160 + 'Content-Type': 'text/html; charset=UTF-8',
161 + 'Cookie': 'pp_session_id=' + obj.authCookie
162 + }
163 + }
164 + const req = https.request(options, function (res) {
165 + if (obj.state == 0) return;
166 + if (res.statusCode != 302) { setState(0); return; }
167 + if (res.headers['set-cookie'] != null) { for (var i in res.headers['set-cookie']) { if (res.headers['set-cookie'][i].startsWith('pp_session_id=')) { obj.authCookie = res.headers['set-cookie'][i].substring(14).split(';')[0]; } } }
168 + res.on('data', function (d) { })
169 + });
170 + req.on('error', function (error) { setState(0); })
171 + req.end();
172 + }
173 +
174 + function fetchInitialInformation() {
175 + fetch('/webs_cron.asp?_portsstatushash=&_devicesstatushash=&webs_job=sidebarupdates', null, null, function (server, tag, data) {
176 + const parsed = parseJsScript(data);
177 + for (var i in parsed['updateSidebarPanel']) {
178 + if (parsed['updateSidebarPanel'][i][0] == "cron_device") {
179 + obj.firmwareVersion = getSubString(parsed['updateSidebarPanel'][i][1], "Firmware: ", "<");
180 + obj.deviceModel = getSubString(parsed['updateSidebarPanel'][i][1], "<div class=\"device-model\">", "<");
181 + }
182 + }
183 + fetch('/sidebar.asp', null, null, function (server, tag, data) {
184 + var dataBlock = getSubString(data, "updateKVMLinkHintOnContainer();", "devices.resetDevicesNew(1);");
185 + if (dataBlock == null) { setState(0); return; }
186 + const parsed = parseJsScript(dataBlock);
187 + obj.portCount = parseInt(parsed['updatePortStatus'][0][0]) - 2;
188 + obj.portHash = parsed['updatePortStatus'][0][1];
189 + obj.deviceCount = parseInt(parsed['updateDeviceStatus'][0][0]);
190 + obj.deviceHash = parsed['updateDeviceStatus'][0][1];
191 + var updatedPorts = [];
192 + for (var i = 0; i < parsed['addPortNew'].length; i++) {
193 + const portInfo = parsePortInfo(parsed['addPortNew'][i]);
194 + obj.ports[portInfo.hIndex] = portInfo;
195 + updatedPorts.push(portInfo.hIndex);
196 + }
197 + setState(2);
198 + if (obj.onPortsChanged != null) { obj.onPortsChanged(obj, updatedPorts); }
199 + });
200 + });
201 + }
202 +
203 + obj.update = function () {
204 + fetch('/webs_cron.asp?_portsstatushash=' + obj.portHash + '&_devicesstatushash=' + obj.deviceHash, null, null, function (server, tag, data) {
205 + const parsed = parseJsScript(data);
206 + if (parsed['updatePortStatus']) {
207 + obj.portCount = parseInt(parsed['updatePortStatus'][0][0]) - 2;
208 + obj.portHash = parsed['updatePortStatus'][0][1];
209 + }
210 + if (parsed['updateDeviceStatus']) {
211 + obj.deviceCount = parseInt(parsed['updateDeviceStatus'][0][0]);
212 + obj.deviceHash = parsed['updateDeviceStatus'][0][1];
213 + }
214 + if (parsed['updatePort']) {
215 + var updatedPorts = [];
216 + for (var i = 0; i < parsed['updatePort'].length; i++) {
217 + const portInfo = parsePortInfo(parsed['updatePort'][i]);
218 + obj.ports[portInfo.hIndex] = portInfo;
219 + updatedPorts.push(portInfo.hIndex);
220 + }
221 + if ((updatedPorts.length > 0) && (obj.onPortsChanged != null)) { obj.onPortsChanged(obj, updatedPorts); }
222 + }
223 + });
224 + }
225 +
226 + function parsePortInfo(args) {
227 + var out = {};
228 + for (var i = 0; i < args.length; i++) {
229 + var parsed = parseJsScript(args[i]);
230 + var v = parsed.J[0][1], vv = parseInt(v);
231 + out[parsed.J[0][0]] = (v == vv)?vv:v;
232 + }
233 + return out;
234 + }
235 +
236 + function getSubString(str, start, end) {
237 + var i = str.indexOf(start);
238 + if (i < 0) return null;
239 + str = str.substring(i + start.length);
240 + i = str.indexOf(end);
241 + if (i >= 0) { str = str.substring(0, i); }
242 + return str;
243 + }
244 +
245 + // Parse JavaScript code calls
246 + function parseJsScript(str) {
247 + const out = {};
248 + var functionName = '';
249 + var args = [];
250 + var arg = null;
251 + var stack = [];
252 + for (var i = 0; i < str.length; i++) {
253 + if (stack.length == 0) {
254 + if (str[i] != '(') {
255 + if (isAlphaNumeric(str[i])) { functionName += str[i]; } else { functionName = ''; }
256 + } else {
257 + stack.push(')');
258 + }
259 + } else {
260 + if (str[i] == stack[stack.length - 1]) {
261 + if (stack.length > 1) { if (arg == null) { arg = str[i]; } else { arg += str[i]; } }
262 + if (stack.length == 2) {
263 + if (arg != null) { args.push(trimQuotes(arg)); }
264 + arg = null;
265 + } else if (stack.length == 1) {
266 + if (arg != null) { args.push(trimQuotes(arg)); arg = null; }
267 + if (args.length > 0) {
268 + if (out[functionName] == null) {
269 + out[functionName] = [args];
270 + } else {
271 + out[functionName].push(args);
272 + }
273 + }
274 + args = [];
275 + }
276 + stack.pop();
277 + } else if ((str[i] == '\'') || (str[i] == '"') || (str[i] == '(')) {
278 + if (str[i] == '(') { stack.push(')'); } else { stack.push(str[i]); }
279 + if (stack.length > 0) {
280 + if (arg == null) { arg = str[i]; } else { arg += str[i]; }
281 + }
282 + } else {
283 + if ((stack.length == 1) && (str[i] == ',')) {
284 + if (arg != null) { args.push(trimQuotes(arg)); arg = null; }
285 + } else {
286 + if (stack.length > 0) { if (arg == null) { arg = str[i]; } else { arg += str[i]; } }
287 + }
288 + }
289 + }
290 + }
291 + return out;
292 + }
293 +
294 + function trimQuotes(str) {
295 + if ((str == null) || (str.length < 2)) return str;
296 + str = str.trim();
297 + if ((str[0] == '\'') && (str[str.length - 1] == '\'')) { return str.substring(1, str.length - 1); }
298 + if ((str[0] == '"') && (str[str.length - 1] == '"')) { return str.substring(1, str.length - 1); }
299 + return str;
300 + }
301 +
302 + function isAlphaNumeric(char) {
303 + return ((char >= 'A') && (char <= 'Z')) || ((char >= 'a') && (char <= 'z')) || ((char >= '0') && (char <= '9'));
304 + }
305 +
306 + function fetch(url, postdata, tag, func) {
307 + if (obj.state == 0) return;
308 + var data = '';
309 + const options = {
310 + hostname: hostname,
311 + port: port,
312 + rejectUnauthorized: false,
313 + checkServerIdentity: onCheckServerIdentity,
314 + path: url,
315 + method: (postdata != null)?'POST':'GET',
316 + headers: {
317 + 'Content-Type': 'text/html; charset=UTF-8',
318 + 'Cookie': 'pp_session_id=' + obj.authCookie
319 + }
320 + }
321 + const req = https.request(options, function (res) {
322 + if (obj.state == 0) return;
323 + if (res.statusCode != 200) { setState(0); return; }
324 + if (res.headers['set-cookie'] != null) { for (var i in res.headers['set-cookie']) { if (res.headers['set-cookie'][i].startsWith('pp_session_id=')) { obj.authCookie = res.headers['set-cookie'][i].substring(14).split(';')[0]; } } }
325 + res.on('data', function (d) { data += d; });
326 + res.on('end', function () { func(obj, tag, data); });
327 + });
328 + req.on('error', function (error) { setState(0); })
329 + req.end();
330 + }
331 +
332 + return obj;
333 +}
334 +
335 +module.exports.CreateIPKVMManager = CreateIPKVMManager;
\ No newline at end of file
meshuser.js
+3 -3
@@ -2513,7 +2513,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
2513
2514 // Event the device group creation
2515 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, action: 'createmesh', links: links, msgid: 76, msgArgs: [command.meshname], msg: 'Device group created: ' + command.meshname, domain: domain.id, creation: mesh.creation, creatorid: mesh.creatorid, creatorname: mesh.creatorname, flags: mesh.flags, consent: mesh.consent };
2516 - parent.parent.DispatchEvent(['*', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
2516 + parent.parent.DispatchEvent(['*', 'server-createmesh', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
2517
2518 // Log in the auth log
2519 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created device group ' + mesh.name); }
@@ -2559,8 +2559,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
2559 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } return; }
2560
2561 // Fire the removal event first, because after this, the event will not route
2562 - var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: command.meshid, name: command.meshname, action: 'deletemesh', msgid: 77, msgArgs: [command.meshname], msg: 'Device group deleted: ' + command.meshname, domain: domain.id };
2563 - parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid), obj, event); // Even if DB change stream is active, this event need to be acted on.
2562 + var event = { etype: 'mesh', userid: user._id, username: user.name, mtype: mesh.mtype, meshid: command.meshid, name: command.meshname, action: 'deletemesh', msgid: 77, msgArgs: [command.meshname], msg: 'Device group deleted: ' + command.meshname, domain: domain.id };
2563 + parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, ['server-deletemesh']), obj, event); // Even if DB change stream is active, this event need to be acted on.
2564
2565 // Remove all user links to this mesh
2566 for (var j in mesh.links) {
webserver.js
+4 -1
@@ -31,7 +31,7 @@ if (!String.prototype.startsWith) { String.prototype.startsWith = function (sear
31 if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchString, position) { var subjectString = this.toString(); if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.lastIndexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }; }
32
33 // Construct a HTTP server object
34 -module.exports.CreateWebServer = function (parent, db, args, certificates) {
34 +module.exports.CreateWebServer = function (parent, db, args, certificates, doneFunc) {
35 var obj = {}, i = 0;
36
37 // Modules
@@ -6307,6 +6307,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
6307
6308 // Start on a second agent-only alternative port if needed.
6309 if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
6310 +
6311 + // We are done starting the web server.
6312 + if (doneFunc) doneFunc();
6313 }
6314
6315 // Perform server inner authentication