Added MeshAgent power actions

Ylian Saint-Hilaire committed Sep 1, 2017 at 11:23 UTC d8464ddd449bc321bf0860c78de00eab00e86a9f
10 files changed +124 -37
agents/MeshService.exe
Binary files a/agents/MeshService.exe and b/agents/MeshService.exe differ
agents/MeshService64.exe
Binary files a/agents/MeshService64.exe and b/agents/MeshService64.exe differ
agents/meshcore.js
+37 -5
@@ -46,8 +46,13 @@ function createMeshCore(agent) {
46 }
47
48 // Get our location (lat/long) using our public IP address
49 + var getIpLocationDataExInProgress = false;
50 + var getIpLocationDataExCounts = [ 0, 0 ];
51 function getIpLocationDataEx(func) {
52 + if (getIpLocationDataExInProgress == true) { return false; }
53 try {
54 + getIpLocationDataExInProgress = true;
55 + getIpLocationDataExCounts[0]++;
56 http.request({
57 host: 'ipinfo.io', // TODO: Use a HTTP proxy if needed!!!!
58 port: 80,
@@ -60,11 +65,13 @@ function createMeshCore(agent) {
65 resp.end = function () {
66 var location = null;
67 try { if (typeof geoData == 'string') { var result = JSON.parse(geoData); if (result.ip && result.loc) { location = result; } } } catch (e) { }
63 - if (func) { func(location); }
68 + if (func) { getIpLocationDataExCounts[1]++; func(location); }
69 }
70 + getIpLocationDataExInProgress = false;
71 }).end();
72 + return true;
73 }
67 - catch (e) { }
74 + catch (e) { return false; }
75 }
76
77 // Remove all Gateway MAC addresses for interface list. This is useful because the gateway MAC is not always populated reliably.
@@ -247,6 +254,18 @@ function createMeshCore(agent) {
254 // TODO!!!!
255 break;
256 }
257 + case 'poweraction': {
258 + // Server telling us to execute a power action
259 + if ((mesh.ExecPowerState != undefined) && (data.actiontype)) {
260 + var forced = 0;
261 + if (data.forced == 1) { forced = 1; }
262 + data.actiontype = parseInt(data.actiontype);
263 + sendConsoleText('Performing power action=' + data.actiontype + ', forced=' + forced + '.');
264 + var r = mesh.ExecPowerState(data.actiontype, forced);
265 + sendConsoleText('ExecPowerState returned code: ' + r);
266 + }
267 + break;
268 + }
269 case 'location': {
270 // Update the location information of this node
271 getIpLocationData(function (location) { mesh.SendCommand({ "action": "location", "type": "publicip", "value": location }); });
@@ -520,7 +539,7 @@ function createMeshCore(agent) {
539 var response = null;
540 switch (cmd) {
541 case 'help': { // Displays available commands
523 - response = 'Available commands: help, info, args, print, type, dbget, dbset, dbcompact, parseurl, httpget, wsconnect, wssend, wsclose, notify, ls, amt, netinfo, location.';
542 + response = 'Available commands: help, info, args, print, type, dbget, dbset, dbcompact, parseurl, httpget, wsconnect, wssend, wsclose, notify, ls, amt, netinfo, location, power.';
543 break;
544 }
545 case 'notify': { // Send a notification message to the mesh
@@ -727,8 +746,21 @@ function createMeshCore(agent) {
746 sendConsoleText(args['_'].join(' '));
747 break;
748 }
730 - case 'location': {
731 - getIpLocationData(function (location) { sendConsoleText("Public IP location:\r\n" + objToString(location, 0, '.'), sessionid); }, args['_'][0]);
749 + case 'location': { // Get location information about this computer
750 + getIpLocationData(function (location) { sendConsoleText('IpLocation: ' + getIpLocationDataExCounts[0] + ' querie(s), ' + getIpLocationDataExCounts[1] + ' response(s), inProgress: ' + getIpLocationDataExInProgress + "\r\nPublic IP location data:\r\n" + objToString(location, 0, '.'), sessionid); }, args['_'][0]);
751 + break;
752 + }
753 + case 'power': { // Execute a power action on this computer
754 + if (mesh.ExecPowerState == undefined) {
755 + response = 'Power command not supported on this agent.';
756 + } else {
757 + if ((args['_'].length == 0) || (typeof args['_'][0] != 'number')) {
758 + response = 'Proper usage: power (actionNumber), where actionNumber is:\r\n LOGOFF = 1\r\n SHUTDOWN = 2\r\n REBOOT = 3\r\n SLEEP = 4\r\n HIBERNATE = 5\r\n DISPLAYON = 6\r\n KEEPAWAKE = 7\r\n BEEP = 8\r\n CTRLALTDEL = 9\r\n VIBRATE = 13\r\n FLASH = 14'; // Display correct command usage
759 + } else {
760 + var r = mesh.ExecPowerState(args['_'][0], args['_'][1]);
761 + response = 'Power action executed with return code: ' + r + '.';
762 + }
763 + }
764 break;
765 }
766 default: { // This is an unknown command, return an error message
meshagent.js
+12 -10
@@ -26,14 +26,16 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
26 obj.agentInfo;
27 obj.agentUpdate = null;
28 var agentUpdateBlockSize = 65520;
29 + obj.remoteaddr = obj.ws._socket.remoteAddress;
30 + if (obj.remoteaddr.startsWith('::ffff:')) { obj.remoteaddr = obj.remoteaddr.substring(7); }
31
32 // Send a message to the mesh agent
33 obj.send = function (data) { if (typeof data == 'string') { obj.ws.send(new Buffer(data, 'binary')); } else { obj.ws.send(data); } }
34
35 // Disconnect this agent
36 obj.close = function (arg) {
35 - if ((arg == 1) || (arg == null)) { try { obj.ws.close(); obj.parent.parent.debug(1, 'Soft disconnect ' + obj.nodeid); } catch (e) { console.log(e); } } // Soft close, close the websocket
36 - if (arg == 2) { try { obj.ws._socket._parent.end(); obj.parent.parent.debug(1, 'Hard disconnect ' + obj.nodeid); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
37 + if ((arg == 1) || (arg == null)) { try { obj.ws.close(); obj.parent.parent.debug(1, 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket
38 + if (arg == 2) { try { obj.ws._socket._parent.end(); obj.parent.parent.debug(1, 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
39 if (obj.parent.wsagents[obj.dbNodeKey] == obj) {
40 delete obj.parent.wsagents[obj.dbNodeKey];
41 obj.parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1);
@@ -211,7 +213,8 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
213 ws.on('error', function (err) { console.log(err); });
214
215 // If the mesh agent web socket is closed, clean up.
214 - ws.on('close', function (req) { obj.close(0); });
216 + ws.on('close', function (req) { obj.parent.parent.debug(1, 'Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); obj.close(0); });
217 + // obj.ws._socket._parent.on('close', function (req) { obj.parent.parent.debug(1, 'Agent TCP disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); });
218
219 // Start authenticate the mesh agent by sending a auth nonce & server TLS cert hash.
220 // Send 256 bits SHA256 hash of TLS cert public key + 256 bits nonce
@@ -223,9 +226,9 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
226 if (obj.authenticated =! 1 || obj.meshid == null) return;
227 // Check that the mesh exists
228 obj.db.Get(obj.dbMeshKey, function (err, meshes) {
226 - if (meshes.length == 0) { console.log('Agent connected with invalid domain/mesh, holding connection.'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
229 + if (meshes.length == 0) { console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddr + ').'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
230 var mesh = meshes[0];
228 - if (mesh.mtype != 2) { console.log('Agent connected with invalid mesh type, holding connection.'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
231 + if (mesh.mtype != 2) { console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddr + ').'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
232
233 // Check that the node exists
234 obj.db.Get(obj.dbNodeKey, function (err, nodes) {
@@ -269,6 +272,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
272 obj.parent.wsagents[obj.dbNodeKey] = obj;
273 if (dupAgent) {
274 // Close the duplicate agent
275 + obj.parent.parent.debug(1, 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddr + ')');
276 dupAgent.close();
277 } else {
278 // Indicate the agent is connected
@@ -306,7 +310,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
310 delete obj.agentnonce;
311 delete obj.unauth;
312 if (obj.unauthsign) delete obj.unauthsign;
309 - obj.parent.parent.debug(1, 'Verified agent connection to ' + obj.nodeid);
313 + obj.parent.parent.debug(1, 'Verified agent connection to ' + obj.nodeid + ' (' + obj.remoteaddr + ').');
314 obj.authenticated = 1;
315 return true;
316 }
@@ -315,7 +319,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
319 function processAgentData(msg) {
320 var str = msg.toString('utf8');
321 if (str[0] == '{') {
318 - try { command = JSON.parse(str) } catch (e) { console.log('Unable to parse JSON'); return; } // If the command can't be parsed, ignore it.
322 + try { command = JSON.parse(str) } catch (e) { console.log('Unable to parse JSON (' + obj.remoteaddr + ').'); return; } // If the command can't be parsed, ignore it.
323 switch (command.action) {
324 case 'msg':
325 {
@@ -434,9 +438,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
438 if (device.intelamt.host != command.intelamt.host) { device.intelamt.host = command.intelamt.host; change = 1; changes.push('AMT host'); }
439 }
440 if (mesh.mtype == 2) {
437 - var remoteaddr = obj.ws._socket.remoteAddress;
438 - if (remoteaddr.startsWith('::ffff:')) { remoteaddr = remoteaddr.substring(7); }
439 - if (device.host != remoteaddr) { device.host = remoteaddr; change = 1; changes.push('host'); }
441 + if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
442 // TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match.
443 }
444
meshrelay.js
+16 -9
@@ -17,9 +17,11 @@ module.exports.CreateMeshRelay = function (parent, ws, req) {
17 var obj = {};
18 obj.ws = ws;
19 obj.peer = null;
20 + obj.parent = parent;
21 obj.id = req.query['id'];
22 + obj.remoteaddr = obj.ws._socket.remoteAddress;
23 + if (obj.remoteaddr.startsWith('::ffff:')) { obj.remoteaddr = obj.remoteaddr.substring(7); }
24
22 - //console.log('Got relay connection for: ' + obj.id);
25 if (obj.id == undefined) { obj.ws.close(); obj.id = null; return null; } // Attempt to connect without id, drop this.
26
27 // Validate that the id is valid, we only need to do this on non-authenticated sessions.
@@ -51,15 +53,18 @@ module.exports.CreateMeshRelay = function (parent, ws, req) {
53 relayinfo.peer1.ws.peer = relayinfo.peer2.ws;
54 relayinfo.peer2.ws.peer = relayinfo.peer1.ws;
55
56 + obj.parent.parent.debug(1, 'Relay connected: ' + obj.id + ' (' + obj.remoteaddr + ' --> ' + obj.peer.remoteaddr + ')');
57 } else {
58 // Connected already, drop (TODO: maybe we should re-connect?)
59 obj.id = null;
60 obj.ws.close();
61 + obj.parent.parent.debug(1, 'Relay duplicate: ' + obj.id + ' (' + obj.remoteaddr + ')');
62 return null;
63 }
64 } else {
65 // Setup the connection, wait for peer
62 - parent.wsrelays[obj.id] = { peer1 : obj, state : 1 };
66 + parent.wsrelays[obj.id] = { peer1: obj, state: 1 };
67 + obj.parent.parent.debug(1, 'Relay holding: ' + obj.id + ' (' + obj.remoteaddr + ')');
68 }
69 }
70
@@ -68,24 +73,26 @@ module.exports.CreateMeshRelay = function (parent, ws, req) {
73 };
74
75 // When data is received from the mesh relay web socket
71 - ws.on('message', function (data)
72 - {
73 - if (this.peer != null) { try { this.pause(); this.peer.send(data, ws.flushSink); } catch (e) { } }
74 - });
76 + ws.on('message', function (data) {
77 + if (this.peer != null) { try { this.pause(); this.peer.send(data, ws.flushSink); } catch (e) { } }
78 + });
79
80 // If error, do nothing
81 ws.on('error', function (err) { console.log(err); });
82
83 // If the mesh relay web socket is closed
84 ws.on('close', function (req) {
81 - //console.log('Got relay disconnection for: ' + obj.id);
85 if (obj.id != null) {
86 var relayinfo = parent.wsrelays[obj.id];
87 if (relayinfo.state == 2) {
88 // Disconnect the peer
86 - var peer = (relayinfo.peer1 == obj)?relayinfo.peer2:relayinfo.peer1;
89 + var peer = (relayinfo.peer1 == obj) ? relayinfo.peer2 : relayinfo.peer1;
90 + obj.parent.parent.debug(1, 'Relay disconnect: ' + obj.id + ' (' + obj.remoteaddr + ' --> ' + peer.remoteaddr + ')');
91 peer.id = null;
88 - peer.ws._socket.end();
92 + try { peer.ws.close(); } catch (e) { } // Soft disconnect
93 + try { peer.ws._socket._parent.end(); } catch (e) { } // Hard disconnect
94 + } else {
95 + obj.parent.parent.debug(1, 'Relay disconnect: ' + obj.id + ' (' + obj.remoteaddr + ')');
96 }
97 delete parent.wsrelays[obj.id];
98 obj.peer = null;
package.json
+1 -1
@@ -1,6 +1,6 @@
1 {
2 "name": "meshcentral",
3 - "version": "0.0.6-v",
3 + "version": "0.0.6-x",
4 "keywords": [
5 "Remote Management",
6 "Intel AMT",
public/scripts/agent-redir-ws-0.1.0.js
+1
@@ -30,6 +30,7 @@ var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort) {
30 obj.socket = new WebSocket(url);
31 obj.socket.onopen = obj.xxOnSocketConnected;
32 obj.socket.onmessage = obj.xxOnMessage;
33 + obj.socket.onerror = function (e) { console.error(e); }
34 obj.socket.onclose = obj.xxOnSocketClosed;
35 obj.xxStateChange(1);
36 obj.meshserver.Send({ action: 'msg', type: 'tunnel', nodeid: obj.nodeid, value: url2 });
views/default.handlebars
+22 -10
@@ -1535,25 +1535,29 @@
1535 }
1536
1537 function groupActionFunction() {
1538 - var x = "Select an operation to perform on all selected devices.<br /><br />";
1539 - x += addHtmlValue('Operation', '<select id=d2groupop style=float:right;width:250px><option value=1>Wake-up devices</option><option value=2>Delete devices</option></select>');
1538 + var x = "Select an operation to perform on all selected devices. Actions will be performed only with proper rights.<br /><br />";
1539 + x += addHtmlValue('Operation', '<select id=d2groupop style=float:right;width:250px><option value=100>Wake-up devices</option><option value=4>Sleep devices</option><option value=3>Reset devices</option><option value=2>Power off devices</option><option value=101>Delete devices</option></select>');
1540 setDialogMode(2, "Group Action", 3, groupActionFunctionEx, x);
1541 }
1542
1543 function groupActionFunctionEx() {
1544 var op = Q('d2groupop').value;
1545 - if (op == 1) {
1545 + if (op == 100) {
1546 // Group wake
1547 var nodeids = [], elements = document.getElementsByClassName("DeviceCheckbox"), checkcount = 0;
1548 for (var i in elements) { if (elements[i].checked) { nodeids.push(elements[i].value.substring(6)); } }
1549 meshserver.Send({ action: 'wakedevices', nodeids: nodeids });
1550 - }
1551 - if (op == 2) {
1550 + } else if (op == 101) {
1551 // Group delete, ask for confirmation
1552 var x = "Confirm delete selected devices(s)?<br /><br />";
1553 x += "<input id=d2check type=checkbox onchange=d2groupActionFunctionDelEx() />Confirm";
1554 setDialogMode(2, "Delete Nodes", 3, groupActionFunctionDelEx, x);
1555 QE('idx_dlgOkButton', false);
1556 + } else {
1557 + // Power operation
1558 + var nodeids = [], elements = document.getElementsByClassName("DeviceCheckbox"), checkcount = 0;
1559 + for (var i in elements) { if (elements[i].checked) { nodeids.push(elements[i].value.substring(6)); } }
1560 + meshserver.Send({ action: 'poweraction', nodeids: nodeids, actiontype: op });
1561 }
1562 }
1563
@@ -2463,9 +2467,9 @@
2467 Q('p14iframe').contentWindow.setAuthCallback(updateAmtCredentials);
2468
2469 // Display "action" button on desktop/terminal/files
2466 - QV('deskActionsBtn', (meshrights & 76) != 0);
2467 - QV('termActionsBtn', (meshrights & 76) != 0);
2468 - QV('filesActionsBtn', (meshrights & 76) != 0);
2470 + QV('deskActionsBtn', (meshrights & 72) != 0); // 72 = Wake-up + Remote Control permissions
2471 + QV('termActionsBtn', (meshrights & 72) != 0);
2472 + QV('filesActionsBtn', (meshrights & 72) != 0);
2473
2474 // Request the power timeline
2475 if ((powerTimelineNode != currentNode._id) && (powerTimelineReq != currentNode._id)) { powerTimelineReq = currentNode._id; meshserver.Send({ action: 'powertimeline', nodeid: currentNode._id }); }
@@ -2475,16 +2479,24 @@
2479 }
2480
2481 function deviceActionFunction() {
2482 + var meshrights = meshes[currentNode.meshid].links['user/{{{domain}}}/' + userinfo.name.toLowerCase()].rights;
2483 var x = "Select an operation to perform on this device.<br /><br />";
2479 - x += addHtmlValue('Operation', '<select id=d2deviceop style=float:right;width:250px><option value=1>Wake-up</option></select>');
2484 + var y = '<select id=d2deviceop style=float:right;width:250px>';
2485 + if ((meshrights & 64) != 0) { y += '<option value=100>Wake-up</option>'; } // Wake-up permission
2486 + if ((meshrights & 8) != 0) { y += '<option value=4>Sleep</option><option value=3>Reset</option><option value=2>Power off</option>'; } // Remote control permission
2487 + y += '</select>';
2488 + x += addHtmlValue('Operation', y);
2489 setDialogMode(2, "Device Action", 3, deviceActionFunctionEx, x);
2490 }
2491
2492 function deviceActionFunctionEx() {
2493 var op = Q('d2deviceop').value;
2485 - if (op == 1) {
2494 + if (op == 100) {
2495 // Device wake
2496 meshserver.Send({ action: 'wakedevices', nodeids: [ currentNode._id ] });
2497 + } else {
2498 + // Power operation
2499 + meshserver.Send({ action: 'poweraction', nodeids: [ currentNode._id ], actiontype: op });
2500 }
2501 }
2502
views/login.handlebars
-2
@@ -146,8 +146,6 @@
146 else if (passStrength >= 60) { QH('passWarning', '<span style=color:blue><b>Good Password</b><span>'); }
147 else { QH('passWarning', '<span style=color:red><b>Weak Password</b><span>'); }
148 }
149 -
150 - console.log(passStrength);
149 }
150
151 // Return a password strength score
webserver.js
+35
@@ -1391,6 +1391,7 @@ module.exports.CreateWebServer = function (parent, db, args, secret, certificate
1391 }
1392 case 'wakedevices':
1393 {
1394 + // TODO: INPUT VALIDATION!!!
1395 // TODO: We can optimize this a lot.
1396 // - We should get a full list of all MAC's to wake first.
1397 // - We should try to only have one agent per subnet (using Gateway MAC) send a wake-on-lan.
@@ -1443,6 +1444,40 @@ module.exports.CreateWebServer = function (parent, db, args, secret, certificate
1444 ws.send(JSON.stringify({ action: 'wakedevices' }));
1445 }
1446
1447 + break;
1448 + }
1449 + case 'poweraction':
1450 + {
1451 + // TODO: INPUT VALIDATION!!!
1452 + for (var i in command.nodeids) {
1453 + var nodeid = command.nodeids[i], powerActions = 0;
1454 + if ((nodeid.split('/').length == 3) && (nodeid.split('/')[1] == domain.id)) { // Validate the domain, operation only valid for current domain
1455 + // Get the device
1456 + obj.db.Get(nodeid, function (err, nodes) {
1457 + if (nodes.length != 1) return;
1458 + var node = nodes[0];
1459 +
1460 + // Get the mesh for this device
1461 + var mesh = obj.meshes[node.meshid];
1462 + if (mesh) {
1463 +
1464 + // Check if this user has rights to do this
1465 + if (mesh.links[user._id] != undefined && ((mesh.links[user._id].rights & 8) != 0)) { // "Remote Control permission"
1466 +
1467 + // Get this device
1468 + var agent = obj.wsagents[node._id];
1469 + if (agent != null) {
1470 + // Send the power command
1471 + agent.send(JSON.stringify({ action: 'poweraction', actiontype: command.actiontype }));
1472 + powerActions++;
1473 + }
1474 + }
1475 + }
1476 + });
1477 + }
1478 + // Confirm we may be doing something (TODO)
1479 + ws.send(JSON.stringify({ action: 'poweraction' }));
1480 + }
1481 break;
1482 }
1483 case 'getnetworkinfo':