Early work on WebPowerSwitch support.

Ylian Saint-Hilaire committed Dec 14, 2021 at 03:22 UTC 1567594ef33719b5206fa6177a1f99ab2bad7d96
3 files changed +260 -21
meshipkvm.js
+258 -19
@@ -47,13 +47,13 @@ function CreateIPKVMManager(parent) {
47 stopManagement(event.meshid);
48 }
49 }
50 -
50 +
51 // Run thru the list of device groups that require
52 for (var i in parent.webserver.meshes) {
53 const mesh = parent.webserver.meshes[i];
54 if ((mesh.mtype == 4) && (mesh.deleted == null)) { startManagement(mesh); }
55 }
56 -
56 +
57 // Start managing a IP KVM device
58 function startManagement(mesh) {
59 if ((mesh == null) || (mesh.mtype != 4) || (mesh.kvm == null) || (mesh.deleted != null) || (obj.managedGroups[mesh._id] != null)) return;
@@ -68,8 +68,17 @@ function CreateIPKVMManager(parent) {
68 manager.onPortsChanged = onPortsChanged;
69 manager.start();
70 }
71 + else if (mesh.kvm.model == 2) { // WebPowerSwitch 7
72 + const manager = CreateWebPowerSwitch(obj, host, port, mesh.kvm.user, mesh.kvm.pass);
73 + manager.meshid = mesh._id;
74 + manager.domainid = mesh._id.split('/')[1];
75 + obj.managedGroups[mesh._id] = manager;
76 + manager.onStateChanged = onStateChanged;
77 + manager.onPortsChanged = onPortsChanged;
78 + manager.start();
79 + }
80 }
72 -
81 +
82 // Stop managing a IP KVM device
83 function stopManagement(meshid) {
84 const manager = obj.managedGroups[meshid];
@@ -86,24 +95,60 @@ function CreateIPKVMManager(parent) {
95 manager.stop();
96 }
97 }
89 -
98 +
99 // Called when a KVM device changes state
100 function onStateChanged(sender, state) {
101 /*
102 console.log('State: ' + ['Disconnected', 'Connecting', 'Connected'][state]);
103 if (state == 2) {
95 - console.log('DeviceModel:', sender.deviceModel);
96 - console.log('FirmwareVersion:', sender.firmwareVersion);
104 + if (sender.deviceModel) { console.log('DeviceModel:', sender.deviceModel); }
105 + if (sender.firmwareVersion) { console.log('FirmwareVersion:', sender.firmwareVersion); }
106 }
107 */
108 }
100 -
109 +
110 // Called when a KVM device changes state
111 function onPortsChanged(sender, updatedPorts) {
112 for (var i = 0; i < updatedPorts.length; i++) {
113 const port = sender.ports[updatedPorts[i]];
114 const nodeid = generateIpKvmNodeId(sender.meshid, port.PortId, sender.domainid);
106 - if ((port.Status == 1) && (port.Class == 'KVM')) {
115 + if ((port.Status == 1) && (port.Class == 'PDU')) {
116 + //console.log(port.PortNumber + ', ' + port.PortId + ', ' + port.Name + ', ' + port.State);
117 + if ((obj.managedPorts[nodeid] == null) || (obj.managedPorts[nodeid].name != port.Name)) {
118 + parent.db.Get(nodeid, function (err, nodes) {
119 + if ((err != null) || (nodes == null)) return;
120 + const mesh = parent.webserver.meshes[sender.meshid];
121 + if (nodes.length == 0) {
122 + // The device does not exist, create it
123 + const device = { type: 'node', mtype: 4, _id: nodeid, icon: 1, meshid: sender.meshid, name: port.Name, rname: port.Name, domain: sender.domainid, portid: port.PortId, portnum: port.PortNumber };
124 + parent.db.Set(device);
125 +
126 + // Event the new node
127 + parent.DispatchEvent(parent.webserver.CreateMeshDispatchTargets(sender.meshid, [nodeid]), obj, { etype: 'node', action: 'addnode', nodeid: nodeid, node: device, msgid: 57, msgArgs: [port.Name, mesh.name], msg: ('Added device ' + port.Name + ' to device group ' + mesh.name), domain: sender.domainid });
128 + } else {
129 + // The device exists, update it
130 + var changed = false;
131 + const device = nodes[0];
132 + if (device.rname != port.Name) { device.rname = port.Name; changed = true; } // Update the device port name
133 + if ((mesh.flags) && (mesh.flags & 2) && (device.name != port.Name)) { device.name = port.Name; changed = true; } // Sync device name to port name
134 + if (changed) {
135 + // Update the database and event the node change
136 + parent.db.Set(device);
137 + parent.DispatchEvent(parent.webserver.CreateMeshDispatchTargets(sender.meshid, [nodeid]), obj, { etype: 'node', action: 'changenode', nodeid: nodeid, node: device, domain: sender.domainid, nolog: 1 });
138 + }
139 + }
140 +
141 + // Set the connectivity state if needed
142 + if (obj.managedPorts[nodeid] == null) {
143 + parent.SetConnectivityState(sender.meshid, nodeid, Date.now(), 1, port.State?1:6, null, null);
144 + obj.managedPorts[nodeid] = { name: port.Name, meshid: sender.meshid, portid: port.PortId, portType: port.PortType, portNo: port.PortIndex };
145 + }
146 + });
147 + } else {
148 + // Update connectivity state
149 + parent.SetConnectivityState(sender.meshid, nodeid, Date.now(), 1, port.State ? 1 : 6, null, null);
150 + }
151 + } else if ((port.Status == 1) && (port.Class == 'KVM')) {
152 //console.log(port.PortNumber + ', ' + port.PortId + ', ' + port.Name + ', ' + port.Type + ', ' + ((port.StatAvailable == 0) ? 'Idle' : 'Connected'));
153 if ((obj.managedPorts[nodeid] == null) || (obj.managedPorts[nodeid].name != port.Name)) {
154 parent.db.Get(nodeid, function (err, nodes) {
@@ -247,6 +292,9 @@ function CreateIPKVMManager(parent) {
292 return obj;
293 }
294
295 +
296 +
297 +// Create Raritan Dominion KX III Manager
298 function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
299 const https = require('https');
300 const obj = {};
@@ -408,7 +456,7 @@ function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
456 for (var i = 0; i < args.length; i++) {
457 var parsed = parseJsScript(args[i]);
458 var v = parsed.J[0][1], vv = parseInt(v);
411 - out[parsed.J[0][0]] = (v == vv)?vv:v;
459 + out[parsed.J[0][0]] = (v == vv) ? vv : v;
460 }
461 return out;
462 }
@@ -483,7 +531,7 @@ function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
531 return ((char >= 'A') && (char <= 'Z')) || ((char >= 'a') && (char <= 'z')) || ((char >= '0') && (char <= '9'));
532 }
533
486 - obj.fetch = function(url, postdata, tag, func) {
534 + obj.fetch = function (url, postdata, tag, func) {
535 if (obj.state == 0) return;
536
537 var data = [];
@@ -493,7 +541,7 @@ function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
541 rejectUnauthorized: false,
542 checkServerIdentity: onCheckServerIdentity,
543 path: url,
496 - method: (postdata != null)?'POST':'GET',
544 + method: (postdata != null) ? 'POST' : 'GET',
545 headers: {
546 'Content-Type': 'text/html; charset=UTF-8',
547 'Cookie': 'pp_session_id=' + obj.authCookie
@@ -611,7 +659,7 @@ function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
659 if (this.wsClient) {
660 logDisconnection(this.wsClient);
661 try { this.wsClient.close(); } catch (ex) { }
614 - try {
662 + try {
663 if (this.wsClient.kvmport) {
664 delete this.wsClient.kvmport.wsClient;
665 delete this.wsClient.kvmport;
@@ -644,17 +692,17 @@ function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
692
693 // Clean up
694 try {
647 - if (this.wsBrowser) {
648 - logDisconnection(this.wsBrowser.wsClient);
649 - try { this.wsBrowser.close(); } catch (ex) { }
650 - delete this.wsBrowser.wsClient; delete this.wsBrowser;
651 - }
652 - if (this.kvmport) { delete this.kvmport.wsClient; delete this.kvmport; }
695 + if (this.wsBrowser) {
696 + logDisconnection(this.wsBrowser.wsClient);
697 + try { this.wsBrowser.close(); } catch (ex) { }
698 + delete this.wsBrowser.wsClient; delete this.wsBrowser;
699 + }
700 + if (this.kvmport) { delete this.kvmport.wsClient; delete this.kvmport; }
701 } catch (ex) { console.log(ex); }
702 });
703 reqinfo.kvmport.wsClient.on('error', function (err) {
704 parent.parent.debug('relay', 'IPKVM: Relay websocket error: ' + err);
657 -
705 +
706 });
707 } catch (ex) { console.log(ex); }
708 }
@@ -663,4 +711,195 @@ function CreateRaritanKX3Manager(parent, hostname, port, username, password) {
711 return obj;
712 }
713
714 +
715 +
716 +
717 +// Create WebPowerSwitch Manager
718 +function CreateWebPowerSwitch(parent, hostname, port, username, password) {
719 + port = 80;
720 + const https = require('http');
721 + const crypto = require('crypto');
722 + const obj = {};
723 + var updateTimer = null;
724 + var retryTimer = null;
725 + var challenge = null;
726 + var challengeRetry = 0;
727 +
728 + obj.state = 0; // 0 = Disconnected, 1 = Connecting, 2 = Connected
729 + obj.ports = [];
730 + obj.portCount = 0;
731 + obj.started = false;
732 +
733 + obj.onStateChanged = null;
734 + obj.onPortsChanged = null;
735 +
736 + function onCheckServerIdentity(cert) {
737 + console.log('TODO: Certificate Check');
738 + }
739 +
740 + obj.start = function () {
741 + if (obj.started) return;
742 + obj.started = true;
743 + if (obj.state == 0) connect();
744 + }
745 +
746 + obj.stop = function () {
747 + if (!obj.started) return;
748 + obj.started = false;
749 + if (retryTimer != null) { clearTimeout(retryTimer); retryTimer = null; }
750 + setState(0);
751 + }
752 +
753 + function setState(newState) {
754 + if (obj.state == newState) return;
755 + obj.state = newState;
756 + if (obj.onStateChanged != null) { obj.onStateChanged(obj, newState); }
757 + if ((newState == 2) && (updateTimer == null)) { updateTimer = setInterval(obj.update, 10000); }
758 + if ((newState != 2) && (updateTimer != null)) { clearInterval(updateTimer); updateTimer = null; }
759 + if ((newState == 0) && (obj.started == true) && (retryTimer == null)) { retryTimer = setTimeout(connect, 20000); }
760 + }
761 +
762 + function connect() {
763 + if (obj.state != 0) return;
764 + setState(1); // 1 = Connecting
765 + obj.update();
766 + }
767 +
768 + obj.update = function() {
769 + obj.fetch('/restapi/relay/outlets/all;/=name,physical_state/', 'GET', null, null, function (sender, tag, rdata, res) {
770 + if (res.statusCode == 207) {
771 + var rdata2 = null;
772 + if (rdata != null) { try { rdata2 = JSON.parse(rdata); } catch (ex) { } }
773 + if (Array.isArray(rdata2)) {
774 + obj.portCount = (rdata2.length / 2);
775 + setState(2); // 2 = Connected
776 + const updatedPorts = [];
777 + for (var i = 0; i < (rdata2.length / 2); i++) {
778 + const portname = rdata2[i * 2];
779 + const portstate = rdata2[(i * 2) + 1];
780 + var portchanged = false;
781 + if (obj.ports[i] == null) {
782 + // Add the port
783 + obj.ports[i] = { PortNumber: i, PortId: 'p' + i, Name: portname, Status: 1, State: portstate, Class: 'PDU' };
784 + portchanged = true;
785 + } else {
786 + // Update the port
787 + const port = obj.ports[i];
788 + if (port.Name != portname) { port.Name = portname; portchanged = true; }
789 + if (port.State != portstate) { port.State = portstate; portchanged = true; }
790 + }
791 + if (portchanged) { updatedPorts.push(i); }
792 + }
793 + if ((updatedPorts.length > 0) && (obj.onPortsChanged != null)) { obj.onPortsChanged(obj, updatedPorts); }
794 + } else {
795 + setState(0); // 0 = Disconnected
796 + }
797 + } else {
798 + setState(0); // 0 = Disconnected
799 + }
800 + });
801 + }
802 +
803 + function setPowerState(port, state, func) {
804 + obj.fetch('/restapi/relay/outlets/' + port + '/state/', 'PUT', 'value=' + state, null, function (sender, tag, rdata, res) {
805 + console.log('DATA:', res.statusCode, rdata.toString());
806 + });
807 + }
808 +
809 + obj.fetch = function (url, method, data, tag, func) {
810 + //console.log('fetch', url, method, data, tag);
811 + if (obj.state == 0) return;
812 + if (typeof data == 'string') { data = Buffer.from(data); }
813 +
814 + var rdata = [];
815 + const options = {
816 + hostname: hostname,
817 + port: port,
818 + rejectUnauthorized: false,
819 + checkServerIdentity: onCheckServerIdentity,
820 + path: url,
821 + method: method,
822 + headers: {
823 + 'Content-Type': 'application/x-www-form-urlencoded',
824 + 'accept': 'application/json',
825 + 'X-CSRF': 'x'
826 + }
827 + }
828 +
829 + if (data != null) { options.headers['Content-Length'] = data.length; }
830 +
831 + if (challenge != null) {
832 + const buf = Buffer.alloc(10);
833 + challenge.cnonce = crypto.randomFillSync(buf).toString('hex');
834 + challenge.nc = '00000001';
835 + const ha1 = crypto.createHash('md5');
836 + ha1.update([username, challenge.realm, password].join(':'));
837 + var xha1 = ha1.digest('hex')
838 + const ha2 = crypto.createHash('md5');
839 + ha2.update([options.method, options.path].join(':'));
840 + var xha2 = ha2.digest('hex');
841 + const response = crypto.createHash('md5');
842 + response.update([xha1, challenge.nonce, challenge.nc, challenge.cnonce, challenge.qop, xha2].join(':'));
843 + var requestParams = {
844 + "username": username,
845 + "realm": challenge.realm,
846 + "nonce": challenge.nonce,
847 + "uri": options.path,
848 + "response": response.digest("hex"),
849 + "cnonce": challenge.cnonce,
850 + "opaque": challenge.opaque
851 + };
852 + options.headers = options.headers || {};
853 + options.headers.Authorization = renderDigest(requestParams) + ', algorithm=MD5, nc=' + challenge.nc + ', qop=' + challenge.qop;
854 + }
855 +
856 + const req = https.request(options, function (res) {
857 + if (obj.state == 0) return;
858 + //console.log('res.statusCode', res.statusCode);
859 + //if (res.statusCode != 200) { console.log(res.statusCode, res.headers, Buffer.concat(data).toString()); setState(0); return; }
860 + challengeRetry = 0;
861 + res.on('data', function (d) { rdata.push(d); });
862 + res.on('end', function () {
863 + if (res.statusCode == 401) {
864 + challengeRetry++;
865 + if (challengeRetry > 4) { setState(0); return; }
866 + challenge = parseChallenge(res.headers['www-authenticate']);
867 + obj.fetch(url, method, data, tag, func);
868 + return;
869 + } else {
870 + // This line is used for debugging only, used to swap a file.
871 + func(obj, tag, Buffer.concat(rdata), res);
872 + }
873 + });
874 + });
875 + req.on('error', function (error) { console.log(error); setState(0); });
876 + req.on('timeout', function () { setState(0); });
877 + if (data) { req.write(data); }
878 + req.end();
879 + }
880 +
881 + function parseChallenge(header) {
882 + header = header.replace('qop="auth,auth-int"', 'qop="auth"'); // We don't support auth-int yet, easiest way to get rid of it.
883 + var prefix = 'Digest ';
884 + var challenge = header.substr(header.indexOf(prefix) + prefix.length);
885 + var parts = challenge.split(',');
886 + var length = parts.length;
887 + var params = {};
888 + for (var i = 0; i < length; i++) {
889 + var part = parts[i].match(/^\s*?([a-zA-Z0-0]+)="(.*)"\s*?$/);
890 + if (part && part.length > 2) { params[part[1]] = part[2]; }
891 + }
892 + return params;
893 + }
894 +
895 + function renderDigest(params) {
896 + var parts = [];
897 + for (var i in params) { parts.push(i + '="' + params[i] + '"'); }
898 + return 'Digest ' + parts.join(', ');
899 + }
900 +
901 + return obj;
902 +}
903 +
904 +
905 module.exports.CreateIPKVMManager = CreateIPKVMManager;
\ No newline at end of file
meshuser.js
+1 -1
@@ -1940,7 +1940,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1940 else if ((parent.args.wanonly == true) && (command.meshtype == 3)) { err = 'Invalid group type'; } // Local device group type is not allowed in WAN mode
1941 else if ((domain.ipkvm == null) && (command.meshtype == 4)) { err = 'Invalid group type'; } // IP KVM device group type is not allowed unless enabled
1942 if ((err == null) && (command.meshtype == 4)) {
1943 - if (command.kvmmodel !== 1) { err = 'Invalid KVM model'; }
1943 + if ((command.kvmmodel < 1) || (command.kvmmodel > 2)) { err = 'Invalid KVM model'; }
1944 else if (common.validateString(command.kvmhost, 1, 128) == false) { err = 'Invalid KVM hostname'; }
1945 else if (common.validateString(command.kvmuser, 1, 128) == false) { err = 'Invalid KVM username'; }
1946 else if (common.validateString(command.kvmpass, 1, 128) == false) { err = 'Invalid KVM password'; }
views/default.handlebars
+1 -1
@@ -11361,7 +11361,7 @@
11361 x += addHtmlValue("Type", '<div style=width:230px;margin:0;padding:0><select id=dp2meshtype style=width:100% onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=2>' + "Manage using a software agent" + '</option><option value=1>' + "Intel&reg; AMT only, no agent" + '</option>' + localGroupType + '</select></div>');
11362 x += addHtmlValue("Description", '<div style=width:230px;margin:0;padding:0><textarea id=dp2meshdesc maxlength=1024 style=width:100%;resize:none></textarea></div>');
11363 x += '<div id=d2ipkvm style=display:none><hr />';
11364 - x += addHtmlValue("Model", '<div style=width:230px;margin:0;padding:0><select id=dp2ipkvmmodel style=width:100% onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=1>' + "Raritan Dominion KX III" + '</option></select></div>');
11364 + x += addHtmlValue("Model", '<div style=width:230px;margin:0;padding:0><select id=dp2ipkvmmodel style=width:100% onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,2) ><option value=1>' + "Raritan Dominion KX III" + '</option><option value=2>' + "Web Power Switch 7" + '</option></select></div>');
11365 x += addHtmlValue("Hostname", '<input id=dp2ipkvmhost style=width:230px maxlength=128 onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) />');
11366 x += addHtmlValue("Username", '<input id=dp2ipkvmuser style=width:230px maxlength=128 onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) />');
11367 x += addHtmlValue("Password", '<input id=dp2ipkvmpass type=password style=width:230px maxlength=128 onchange=account_validateMeshCreate() onkeyup=account_validateMeshCreate(event,1) />');