Initial merged script-task into the agent.

Ylian Saint-Hilaire committed Aug 18, 2022 at 16:19 UTC 3aca17ea6d35a650b2a225067f9978de6b62ee12
3 files changed +438 -10
agents/meshcore.js
+6 -8
@@ -708,6 +708,7 @@ db = require('SimpleDataStore').Shared();
708 sha = require('SHA256Stream');
709 mesh = require('MeshAgent');
710 childProcess = require('child_process');
711 +try { scriptTask = require('script-task').CreateScriptTask(mesh); } catch (ex) { }
712
713 if (mesh.hasKVM == 1) { // if the agent is compiled with KVM support
714 // Check if this computer supports a desktop
@@ -798,10 +799,6 @@ function getIpLocationDataEx(func) {
799 catch (ex) { return false; }
800 }
801
801 -// Setup script task. Allows running scripts at scheduled intervals
802 -var scriptTask = null;
803 -try { scriptTask = require('scripttask'); } catch (ex) { }
804 -
802 // Remove all Gateway MAC addresses for interface list. This is useful because the gateway MAC is not always populated reliably.
803 function clearGatewayMac(str) {
804 if (typeof str != 'string') return null;
@@ -1559,6 +1556,10 @@ function handleServerCommand(data) {
1556 try { require(data.plugin).consoleaction(data, data.rights, data.sessionid, this); } catch (ex) { throw ex; }
1557 break;
1558 }
1559 + case 'task': {
1560 + if (scriptTask) { scriptTask.consoleAction(data, data.rights, data.sessionid, false); }
1561 + break;
1562 + }
1563 case 'coredump':
1564 // Set the current agent coredump situation.s
1565 if (data.value === true) {
@@ -4566,10 +4567,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
4567 }
4568 case 'task': {
4569 if (!scriptTask) { response = "Tasks are not supported on this agent"; }
4569 - else {
4570 - if (args['_'][0]) { args.cmd = args['_'][0].toLowerCase(); }
4571 - response = scriptTask.processCommand(args, rights, sessionid);
4572 - }
4570 + else { response = scriptTask.consoleAction(args, rights, sessionid, true); }
4571 break;
4572 }
4573 case 'plugin': {
agents/modules_meshcore/script-task.js new
+416
@@ -0,0 +1,416 @@
1 +/**
2 +* @description MeshCentral Script-Task
3 +* @author Ryan Blenis
4 +* @copyright
5 +* @license Apache-2.0
6 +*/
7 +
8 +'use strict';
9 +function CreateScriptTask(parent) {
10 + var obj = {};
11 + var db = require('SimpleDataStore').Shared();
12 + var pendingDownload = [];
13 + var debugFlag = false;
14 + var runningJobs = [];
15 + var runningJobPIDs = {};
16 +
17 + function dbg(str) {
18 + if (debugFlag !== true) return;
19 + var fs = require('fs');
20 + var logStream = fs.createWriteStream('scripttask.txt', { 'flags': 'a' });
21 + // use {'flags': 'a'} to append and {'flags': 'w'} to erase and write a new file
22 + logStream.write('\n' + new Date().toLocaleString() + ': ' + str);
23 + logStream.end('\n');
24 + }
25 +
26 + function removeFromArray(arr, from, to) {
27 + var rest = arr.slice((to || from) + 1 || arr.length);
28 + arr.length = from < 0 ? arr.length + from : from;
29 + return arr.push.apply(arr, rest);
30 + };
31 +
32 + obj.consoleAction = function(args, rights, sessionid, interactive) {
33 + //sendConsoleText('task: ' + JSON.stringify(args), sessionid); // Debug
34 +
35 + /*
36 + if (typeof args['_'] == 'undefined') {
37 + args['_'] = [];
38 + args['_'][1] = args.pluginaction; // TODO
39 + args['_'][2] = null;
40 + args['_'][3] = null;
41 + args['_'][4] = null;
42 + }
43 + */
44 +
45 + var fnname = args['_'][0];
46 + if (fnname == null) { return "Valid task commands are: trigger, cache, clear, clearCache, debug, list"; }
47 +
48 + switch (fnname.toLowerCase()) {
49 + case 'trigger': {
50 + var jObj = {
51 + jobId: args.jobId,
52 + scriptId: args.scriptId,
53 + replaceVars: args.replaceVars,
54 + scriptHash: args.scriptHash,
55 + dispatchTime: args.dispatchTime
56 + };
57 + //dbg('jObj args is ' + JSON.stringify(jObj));
58 + var sObj = getScriptFromCache(jObj.scriptId);
59 + //dbg('sobj = ' + JSON.stringify(sObj) + ', shash = ' + jObj.scriptHash);
60 + if ((sObj == null) || (sObj.contentHash != jObj.scriptHash)) {
61 + // get from the server, then run
62 + //dbg('Getting and caching script '+ jObj.scriptId);
63 + parent.SendCommand({ action: 'script-task', subaction: 'getScript', scriptId: jObj.scriptId, sessionid: sessionid, tag: 'console' });
64 + pendingDownload.push(jObj);
65 + } else {
66 + // ready to run
67 + runScript(sObj, jObj, sessionid);
68 + }
69 + break;
70 + }
71 + case 'cache': {
72 + var sObj = args.script;
73 + cacheScript(sObj);
74 + var setRun = [];
75 + if (pendingDownload.length) {
76 + pendingDownload.forEach(function (pd, k) {
77 + if ((pd.scriptId == sObj._id) && (pd.scriptHash == sObj.contentHash)) {
78 + if (setRun.indexOf(pd) === -1) { runScript(sObj, pd, sessionid); setRun.push(pd); }
79 + removeFromArray(pendingDownload, k);
80 + }
81 + });
82 + }
83 + break;
84 + }
85 + case 'clear': {
86 + clearCache();
87 + parent.SendCommand({ action: 'script-task', subaction: 'clearAllPendingTasks', sessionid: sessionid, tag: 'console' });
88 + return "Cache cleared. All pending tasks cleared.";
89 + }
90 + case 'clearcache': {
91 + clearCache();
92 + return "The script cache has been cleared";
93 + }
94 + case 'debug': {
95 + debugFlag = (debugFlag) ? false : true;
96 + var str = (debugFlag) ? 'on' : 'off';
97 + return 'Debugging is now ' + str;
98 + }
99 + case 'list': {
100 + var ret = '';
101 + if (pendingDownload.length == 0) return "No tasks pending script download";
102 + pendingDownload.forEach(function (pd, k) { ret += 'Task ' + k + ': ' + 'TaskID: ' + pd.jobId + ' ScriptID: ' + pd.scriptId + '\r\n'; });
103 + return ret;
104 + }
105 + default: {
106 + dbg('Unknown action: ' + fnname + ' with data ' + JSON.stringify(args));
107 + break;
108 + }
109 + }
110 + }
111 +
112 + function finalizeJob(job, retVal, errVal, sessionid) {
113 + if (errVal != null && errVal.stack != null) errVal = errVal.stack;
114 + removeFromArray(runningJobs, runningJobs.indexOf(job.jobId));
115 + if (typeof runningJobPIDs[job.jobId] != 'undefined') delete runningJobPIDs[job.jobId];
116 + parent.SendCommand({
117 + action: 'script-task',
118 + subaction: 'taskComplete',
119 + jobId: job.jobId,
120 + scriptId: job.scriptId,
121 + retVal: retVal,
122 + errVal: errVal,
123 + dispatchTime: job.dispatchTime, // include original run time (long running tasks could have tried a re-send)
124 + sessionid: sessionid,
125 + tag: 'console'
126 + });
127 + }
128 +
129 + //@TODO Test powershell on *nix devices with and without powershell installed
130 + function runPowerShell(sObj, jObj, sessionid) {
131 + if (process.platform != 'win32') return runPowerShellNonWin(sObj, jObj);
132 + const fs = require('fs');
133 + var rand = Math.random().toString(32).replace('0.', '');
134 +
135 + var oName = 'st' + rand + '.txt';
136 + var pName = 'st' + rand + '.ps1';
137 + var pwshout = '', pwsherr = '', cancontinue = false;
138 + try {
139 + fs.writeFileSync(pName, sObj.content);
140 + var outstr = '', errstr = '';
141 + var child = require('child_process').execFile(process.env['windir'] + '\\system32\\WindowsPowerShell\\v1.0\\powershell.exe', ['-NoLogo']);
142 + child.stderr.on('data', function (chunk) { errstr += chunk; });
143 + child.stdout.on('data', function (chunk) { });
144 + runningJobPIDs[jObj.jobId] = child.pid;
145 + child.stdin.write('.\\' + pName + ' | Out-File ' + oName + ' -Encoding UTF8\r\n');
146 + child.on('exit', function (procRetVal, procRetSignal) {
147 + dbg('Exiting with ' + procRetVal + ', Signal: ' + procRetSignal);
148 + if (errstr != '') {
149 + finalizeJob(jObj, null, errstr, sessionid);
150 + try { fs.unlinkSync(oName); fs.unlinkSync(pName); } catch (ex) { dbg('Could not unlink files, error was: ' + ex); }
151 + return;
152 + }
153 + if (procRetVal == 1) {
154 + finalizeJob(jObj, null, 'Process terminated unexpectedly.', sessionid);
155 + try { fs.unlinkSync(oName); fs.unlinkSync(pName); } catch (ex) { dbg('Could not unlink files, error was: ' + ex); }
156 + return;
157 + }
158 + try { outstr = fs.readFileSync(oName, 'utf8').toString(); } catch (ex) { outstr = (procRetVal) ? 'Failure' : 'Success'; }
159 + if (outstr) {
160 + //outstr = outstr.replace(/[^\x20-\x7E]/g, '');
161 + try { outstr = outstr.trim(); } catch (ex) { }
162 + } else {
163 + outstr = (procRetVal) ? 'Failure' : 'Success';
164 + }
165 + dbg('Output is: ' + outstr);
166 + finalizeJob(jObj, outstr, null, sessionid);
167 + try { fs.unlinkSync(oName); fs.unlinkSync(pName); } catch (ex) { }
168 + });
169 + child.stdin.write('exit\r\n');
170 + //child.waitExit(); // this was causing the event loop to stall on long-running scripts, switched to '.on exit'
171 +
172 + } catch (ex) {
173 + dbg('Error block was (PowerShell): ' + ex);
174 + finalizeJob(jObj, null, ex, sessionid);
175 + }
176 + }
177 +
178 + function runPowerShellNonWin(sObj, jObj, sessionid) {
179 + const fs = require('fs');
180 + var rand = Math.random().toString(32).replace('0.', '');
181 +
182 + var path = '';
183 + var pathTests = ['/usr/local/mesh', '/tmp', '/usr/local/mesh_services/meshagent', '/var/tmp'];
184 + pathTests.forEach(function (p) { if (path == '' && fs.existsSync(p)) { path = p; } });
185 + dbg('Path chosen is: ' + path);
186 + path = path + '/';
187 +
188 + var oName = 'st' + rand + '.txt';
189 + var pName = 'st' + rand + '.ps1';
190 + var pwshout = '', pwsherr = '', cancontinue = false;
191 + try {
192 + var childp = require('child_process').execFile('/bin/sh', ['sh']);
193 + childp.stderr.on('data', function (chunk) { pwsherr += chunk; });
194 + childp.stdout.on('data', function (chunk) { pwshout += chunk; });
195 + childp.stdin.write('which pwsh' + '\n');
196 + childp.stdin.write('exit\n');
197 + childp.waitExit();
198 + } catch (ex) { finalizeJob(jObj, null, "Couldn't determine pwsh in env: " + ex, sessionid); }
199 + if (pwsherr != '') { finalizeJob(jObj, null, "PowerShell env determination error: " + pwsherr, sessionid); return; }
200 + if (pwshout.trim() != '') { cancontinue = true; }
201 + if (cancontinue === false) { finalizeJob(jObj, null, "PowerShell is not installed", sessionid); return; }
202 + try {
203 + fs.writeFileSync(path + pName, '#!' + pwshout + '\n' + sObj.content.split('\r\n').join('\n').split('\r').join('\n'));
204 + var outstr = '', errstr = '';
205 + var child = require('child_process').execFile('/bin/sh', ['sh']);
206 + child.stderr.on('data', function (chunk) { errstr += chunk; });
207 + child.stdout.on('data', function (chunk) { });
208 + runningJobPIDs[jObj.jobId] = child.pid;
209 +
210 + child.stdin.write('cd ' + path + '\n');
211 + child.stdin.write('chmod a+x ' + pName + '\n');
212 + child.stdin.write('./' + pName + ' > ' + oName + '\n');
213 + child.on('exit', function (procRetVal, procRetSignal) {
214 + if (errstr != '') {
215 + finalizeJob(jObj, null, errstr, sessionid);
216 + try {
217 + fs.unlinkSync(path + oName);
218 + fs.unlinkSync(path + pName);
219 + } catch (ex) { dbg('Could not unlink files, error was: ' + ex + ' for path ' + path); }
220 + return;
221 + }
222 + if (procRetVal == 1) {
223 + finalizeJob(jObj, null, 'Process terminated unexpectedly.', sessionid);
224 + try {
225 + fs.unlinkSync(path + oName);
226 + fs.unlinkSync(path + pName);
227 + } catch (ex) { dbg('Could not unlink files1, error was: ' + ex + ' for path ' + path); }
228 + return;
229 + }
230 + try { outstr = fs.readFileSync(path + oName, 'utf8').toString(); } catch (es) { outstr = (procRetVal) ? 'Failure' : 'Success'; }
231 + if (outstr) {
232 + //outstr = outstr.replace(/[^\x20-\x7E]/g, '');
233 + try { outstr = outstr.trim(); } catch (ex) { }
234 + } else {
235 + outstr = (procRetVal) ? 'Failure' : 'Success';
236 + }
237 + dbg('Output is: ' + outstr);
238 + finalizeJob(jObj, outstr, null, sessionid);
239 + try { fs.unlinkSync(path + oName); fs.unlinkSync(path + pName); } catch (ex) { dbg('Could not unlink files2, error was: ' + ex + ' for path ' + path); }
240 + });
241 + child.stdin.write('exit\n');
242 + } catch (ex) {
243 + dbg('Error block was (PowerShellNonWin): ' + ex);
244 + finalizeJob(jObj, null, ex, sessionid);
245 + }
246 + }
247 +
248 + function runBat(sObj, jObj, sessionid) {
249 + if (process.platform != 'win32') { finalizeJob(jObj, null, "Platform not supported.", sessionid); return; }
250 + const fs = require('fs');
251 + var rand = Math.random().toString(32).replace('0.', '');
252 + var oName = 'st' + rand + '.txt';
253 + var pName = 'st' + rand + '.bat';
254 + try {
255 + fs.writeFileSync(pName, sObj.content);
256 + var outstr = '', errstr = '';
257 + var child = require('child_process').execFile(process.env['windir'] + '\\system32\\cmd.exe');
258 + child.stderr.on('data', function (chunk) { errstr += chunk; });
259 + child.stdout.on('data', function (chunk) { });
260 + runningJobPIDs[jObj.jobId] = child.pid;
261 + child.stdin.write(pName + ' > ' + oName + '\r\n');
262 + child.stdin.write('exit\r\n');
263 +
264 + child.on('exit', function (procRetVal, procRetSignal) {
265 + if (errstr != '') {
266 + try { fs.unlinkSync(oName); fs.unlinkSync(pName); } catch (ex) { dbg('Could not unlink files, error was: ' + ex); }
267 + finalizeJob(jObj, null, errstr, sessionid);
268 + return;
269 + }
270 + if (procRetVal == 1) {
271 + try { fs.unlinkSync(oName); fs.unlinkSync(pName); } catch (ex) { dbg('Could not unlink files, error was: ' + ex); }
272 + finalizeJob(jObj, null, 'Process terminated unexpectedly.', sessionid);
273 + return;
274 + }
275 + try { outstr = fs.readFileSync(oName, 'utf8').toString(); } catch (ex) { outstr = (procRetVal) ? 'Failure' : 'Success'; }
276 + if (outstr) {
277 + //outstr = outstr.replace(/[^\x20-\x7E]/g, '');
278 + try { outstr = outstr.trim(); } catch (ex) { }
279 + } else {
280 + outstr = (procRetVal) ? 'Failure' : 'Success';
281 + }
282 + dbg('Output is: ' + outstr);
283 + try { fs.unlinkSync(oName); fs.unlinkSync(pName); } catch (ex) { dbg('Could not unlink files, error was: ' + ex); }
284 + finalizeJob(jObj, outstr, null, sessionid);
285 + });
286 + } catch (ex) {
287 + dbg('Error block was (BAT): ' + ex);
288 + finalizeJob(jObj, null, ex, sessionid);
289 + }
290 + }
291 +
292 + function runBash(sObj, jObj, sessionid) {
293 + if (process.platform == 'win32') { finalizeJob(jObj, null, "Platform not supported.", sessionid); return; }
294 + //dbg('proc is ' + JSON.stringify(process));
295 + const fs = require('fs');
296 + var path = '';
297 + var pathTests = ['/usr/local/mesh', '/tmp', '/usr/local/mesh_services/meshagent', '/var/tmp'];
298 + pathTests.forEach(function (p) {
299 + if (path == '' && fs.existsSync(p)) { path = p; }
300 + });
301 + dbg('Path chosen is: ' + path);
302 + path = path + '/';
303 + //var child = require('child_process');
304 + //child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 });
305 +
306 + var rand = Math.random().toString(32).replace('0.', '');
307 + var oName = 'st' + rand + '.txt';
308 + var pName = 'st' + rand + '.sh';
309 + try {
310 + fs.writeFileSync(path + pName, sObj.content);
311 + var outstr = '', errstr = '';
312 + var child = require('child_process').execFile('/bin/sh', ['sh']);
313 + child.stderr.on('data', function (chunk) { errstr += chunk; });
314 + child.stdout.on('data', function (chunk) { });
315 + runningJobPIDs[jObj.jobId] = child.pid;
316 + child.stdin.write('cd ' + path + '\n');
317 + child.stdin.write('chmod a+x ' + pName + '\n');
318 + child.stdin.write('./' + pName + ' > ' + oName + '\n');
319 + child.stdin.write('exit\n');
320 +
321 + child.on('exit', function (procRetVal, procRetSignal) {
322 + if (errstr != '') {
323 + try { fs.unlinkSync(path + oName); fs.unlinkSync(path + pName); } catch (ex) { dbg('Could not unlink files, error was: ' + ex + ' for path ' + path); }
324 + finalizeJob(jObj, null, errstr, sessionid);
325 + return;
326 + }
327 + if (procRetVal == 1) {
328 + try { fs.unlinkSync(path + oName); fs.unlinkSync(path + pName); } catch (ex) { dbg('Could not unlink files1, error was: ' + ex + ' for path ' + path); }
329 + finalizeJob(jObj, null, "Process terminated unexpectedly.", sessionid);
330 + return;
331 + }
332 + try { outstr = fs.readFileSync(path + oName, 'utf8').toString(); } catch (ex) { outstr = (procRetVal) ? 'Failure' : 'Success'; }
333 + if (outstr) {
334 + //outstr = outstr.replace(/[^\x20-\x7E]/g, '');
335 + try { outstr = outstr.trim(); } catch (ex) { }
336 + } else {
337 + outstr = (procRetVal) ? 'Failure' : 'Success';
338 + }
339 + dbg('Output is: ' + outstr);
340 + try { fs.unlinkSync(path + oName); fs.unlinkSync(path + pName); } catch (ex) { dbg('Could not unlink files2, error was: ' + ex + ' for path ' + path); }
341 + finalizeJob(jObj, outstr, null, sessionid);
342 + });
343 + } catch (ex) {
344 + dbg('Error block was (bash): ' + ex);
345 + finalizeJob(jObj, null, ex, sessionid);
346 + }
347 + }
348 +
349 + function jobIsRunning(jObj) {
350 + if (runningJobs.indexOf(jObj.jobId) === -1) return false;
351 + return true;
352 + }
353 +
354 + function runScript(sObj, jObj, sessionid) {
355 + // get current processes and clean running jobs if they are no longer running (computer fell asleep, user caused process to stop, etc.)
356 + if (process.platform != 'linux' && runningJobs.length) { // linux throws errors here in the meshagent for some reason
357 + require('process-manager').getProcesses(function (plist) {
358 + dbg('Got process list');
359 + dbg('There are currently ' + runningJobs.length + ' running jobs.');
360 + if (runningJobs.length) {
361 + runningJobs.forEach(function (jobId, idx) {
362 + dbg('Checking for running job: ' + jobId + ' with PID ' + runningJobPIDs[jobId]);
363 + if (typeof plist[runningJobPIDs[jobId]] == 'undefined' || typeof plist[runningJobPIDs[jobId]].cmd != 'string') {
364 + dbg('Found job with no process. Removing running status.');
365 + delete runningJobPIDs[jobId];
366 + removeFromArray(runningJobs, runningJobs.indexOf(idx));
367 + //dbg('RunningJobs: ' + JSON.stringify(runningJobs));
368 + //dbg('RunningJobsPIDs: ' + JSON.stringify(runningJobPIDs));
369 + }
370 + });
371 + }
372 + });
373 + }
374 + if (jobIsRunning(jObj)) { dbg('Job already running job id [' + jObj.jobId + ']. Skipping.'); return; }
375 + if (jObj.replaceVars != null) {
376 + Object.getOwnPropertyNames(jObj.replaceVars).forEach(function (key) {
377 + var val = jObj.replaceVars[key];
378 + sObj.content = sObj.content.replace(new RegExp('#' + key + '#', 'g'), val);
379 + dbg('replacing var ' + key + ' with ' + val);
380 + });
381 + sObj.content = sObj.content.replace(new RegExp('#(.*?)#', 'g'), 'VAR_NOT_FOUND');
382 + }
383 + runningJobs.push(jObj.jobId);
384 + dbg('Running Script ' + sObj._id);
385 + switch (sObj.filetype) {
386 + case 'ps1': runPowerShell(sObj, jObj, sessionid); break;
387 + case 'bat': runBat(sObj, jObj, sessionid); break;
388 + case 'bash': runBash(sObj, jObj, sessionid); break;
389 + default: dbg('Unknown filetype: ' + sObj.filetype); break;
390 + }
391 + }
392 +
393 + function getScriptFromCache(id) {
394 + var script = db.Get('scriptTask_script_' + id);
395 + if (script == '' || script == null) return null;
396 + try { script = JSON.parse(script); } catch (ex) { return null; }
397 + return script;
398 + }
399 +
400 + function cacheScript(sObj) {
401 + db.Put('scriptTask_script_' + sObj._id, sObj);
402 + }
403 +
404 + function clearCache() {
405 + db.Keys.forEach(function (k) { if (k.indexOf('scriptTask_script_') === 0) { db.Put(k, null); db.Delete(k); } });
406 + }
407 +
408 + function sendConsoleText(text, sessionid) {
409 + if (typeof text == 'object') { text = JSON.stringify(text); }
410 + parent.SendCommand({ action: 'msg', type: 'console', value: 'XXX: ' + text, sessionid: sessionid });
411 + }
412 +
413 + return obj;
414 +}
415 +
416 +module.exports = { CreateScriptTask: CreateScriptTask };
\ No newline at end of file
meshagent.js
+16 -2
@@ -1738,8 +1738,22 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
1738 try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
1739 break;
1740 }
1741 - case 'scriptTask': {
1742 - // TODO
1741 + case 'script-task': {
1742 + // These command are for running regular batch jobs on the remote device
1743 + switch (command.subaction) {
1744 + case 'getScript': {
1745 + console.log('getScript');
1746 + break;
1747 + }
1748 + case 'clearAllPendingTasks': {
1749 + console.log('clearAllPendingTasks');
1750 + break;
1751 + }
1752 + case 'taskComplete': {
1753 + console.log('taskComplete');
1754 + break;
1755 + }
1756 + }
1757 break;
1758 }
1759 default: {