agentrecoverycore.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. var http = require('http');
  2. var childProcess = require('child_process');
  3. var meshCoreObj = { "action": "coreinfo", "value": "MeshCore Recovery", "caps": 14 }; // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript
  4. var nextTunnelIndex = 1;
  5. var tunnels = {};
  6. var fs = require('fs');
  7. //attachDebugger({ webport: 9994, wait: 1 }).then(function (p) { console.log('Debug on port: ' + p); });
  8. function sendConsoleText(msg)
  9. {
  10. require('MeshAgent').SendCommand({ "action": "msg", "type": "console", "value": msg });
  11. }
  12. // Return p number of spaces
  13. function addPad(p, ret) { var r = ''; for (var i = 0; i < p; i++) { r += ret; } return r; }
  14. var path =
  15. {
  16. join: function ()
  17. {
  18. var x = [];
  19. for (var i in arguments)
  20. {
  21. var w = arguments[i];
  22. if (w != null)
  23. {
  24. while (w.endsWith('/') || w.endsWith('\\')) { w = w.substring(0, w.length - 1); }
  25. if (i != 0)
  26. {
  27. while (w.startsWith('/') || w.startsWith('\\')) { w = w.substring(1); }
  28. }
  29. x.push(w);
  30. }
  31. }
  32. if (x.length == 0) return '/';
  33. return x.join('/');
  34. }
  35. };
  36. // Convert an object to string with all functions
  37. function objToString(x, p, pad, ret) {
  38. if (ret == undefined) ret = '';
  39. if (p == undefined) p = 0;
  40. if (x == null) { return '[null]'; }
  41. if (p > 8) { return '[...]'; }
  42. if (x == undefined) { return '[undefined]'; }
  43. if (typeof x == 'string') { if (p == 0) return x; return '"' + x + '"'; }
  44. if (typeof x == 'buffer') { return '[buffer]'; }
  45. if (typeof x != 'object') { return x; }
  46. var r = '{' + (ret ? '\r\n' : ' ');
  47. for (var i in x) { if (i != '_ObjectID') { r += (addPad(p + 2, pad) + i + ': ' + objToString(x[i], p + 2, pad, ret) + (ret ? '\r\n' : ' ')); } }
  48. return r + addPad(p, pad) + '}';
  49. }
  50. // Split a string taking into account the quoats. Used for command line parsing
  51. function splitArgs(str)
  52. {
  53. var myArray = [], myRegexp = /[^\s"]+|"([^"]*)"/gi;
  54. do { var match = myRegexp.exec(str); if (match != null) { myArray.push(match[1] ? match[1] : match[0]); } } while (match != null);
  55. return myArray;
  56. }
  57. // Parse arguments string array into an object
  58. function parseArgs(argv)
  59. {
  60. var results = { '_': [] }, current = null;
  61. for (var i = 1, len = argv.length; i < len; i++) {
  62. var x = argv[i];
  63. if (x.length > 2 && x[0] == '-' && x[1] == '-') {
  64. if (current != null) { results[current] = true; }
  65. current = x.substring(2);
  66. } else {
  67. if (current != null) { results[current] = toNumberIfNumber(x); current = null; } else { results['_'].push(toNumberIfNumber(x)); }
  68. }
  69. }
  70. if (current != null) { results[current] = true; }
  71. return results;
  72. }
  73. // Get server target url with a custom path
  74. function getServerTargetUrl(path)
  75. {
  76. var x = require('MeshAgent').ServerUrl;
  77. //sendConsoleText("mesh.ServerUrl: " + mesh.ServerUrl);
  78. if (x == null) { return null; }
  79. if (path == null) { path = ''; }
  80. x = http.parseUri(x);
  81. if (x == null) return null;
  82. return x.protocol + '//' + x.host + ':' + x.port + '/' + path;
  83. }
  84. // Get server url. If the url starts with "*/..." change it, it not use the url as is.
  85. function getServerTargetUrlEx(url)
  86. {
  87. if (url.substring(0, 2) == '*/') { return getServerTargetUrl(url.substring(2)); }
  88. return url;
  89. }
  90. require('MeshAgent').on('Connected', function ()
  91. {
  92. require('os').name().then(function (v)
  93. {
  94. sendConsoleText("Mesh Agent Receovery Console, OS: " + v);
  95. require('MeshAgent').SendCommand(meshCoreObj);
  96. });
  97. });
  98. // Tunnel callback operations
  99. function onTunnelUpgrade(response, s, head) {
  100. this.s = s;
  101. s.httprequest = this;
  102. s.end = onTunnelClosed;
  103. s.tunnel = this;
  104. //sendConsoleText('onTunnelUpgrade');
  105. if (this.tcpport != null) {
  106. // This is a TCP relay connection, pause now and try to connect to the target.
  107. s.pause();
  108. s.data = onTcpRelayServerTunnelData;
  109. var connectionOptions = { port: parseInt(this.tcpport) };
  110. if (this.tcpaddr != null) { connectionOptions.host = this.tcpaddr; } else { connectionOptions.host = '127.0.0.1'; }
  111. s.tcprelay = net.createConnection(connectionOptions, onTcpRelayTargetTunnelConnect);
  112. s.tcprelay.peerindex = this.index;
  113. } else {
  114. // This is a normal connect for KVM/Terminal/Files
  115. s.data = onTunnelData;
  116. }
  117. }
  118. require('MeshAgent').AddCommandHandler(function (data)
  119. {
  120. if (typeof data == 'object')
  121. {
  122. // If this is a console command, parse it and call the console handler
  123. switch (data.action)
  124. {
  125. case 'msg':
  126. {
  127. switch (data.type)
  128. {
  129. case 'console': { // Process a console command
  130. if (data.value && data.sessionid)
  131. {
  132. var args = splitArgs(data.value);
  133. processConsoleCommand(args[0].toLowerCase(), parseArgs(args), data.rights, data.sessionid);
  134. }
  135. break;
  136. }
  137. case 'tunnel':
  138. {
  139. if (data.value != null) { // Process a new tunnel connection request
  140. // Create a new tunnel object
  141. var xurl = getServerTargetUrlEx(data.value);
  142. if (xurl != null) {
  143. var woptions = http.parseUri(xurl);
  144. woptions.rejectUnauthorized = 0;
  145. //sendConsoleText(JSON.stringify(woptions));
  146. var tunnel = http.request(woptions);
  147. tunnel.on('upgrade', function (response, s, head)
  148. {
  149. this.s = s;
  150. s.httprequest = this;
  151. s.tunnel = this;
  152. s.on('end', function ()
  153. {
  154. if (tunnels[this.httprequest.index] == null) return; // Stop duplicate calls.
  155. // If there is a upload or download active on this connection, close the file
  156. if (this.httprequest.uploadFile) { fs.closeSync(this.httprequest.uploadFile); this.httprequest.uploadFile = undefined; }
  157. if (this.httprequest.downloadFile) { fs.closeSync(this.httprequest.downloadFile); this.httprequest.downloadFile = undefined; }
  158. //sendConsoleText("Tunnel #" + this.httprequest.index + " closed.", this.httprequest.sessionid);
  159. delete tunnels[this.httprequest.index];
  160. // Clean up WebSocket
  161. this.removeAllListeners('data');
  162. });
  163. s.on('data', function (data)
  164. {
  165. // If this is upload data, save it to file
  166. if (this.httprequest.uploadFile)
  167. {
  168. try { fs.writeSync(this.httprequest.uploadFile, data); } catch (e) { this.write(new Buffer(JSON.stringify({ action: 'uploaderror' }))); return; } // Write to the file, if there is a problem, error out.
  169. this.write(new Buffer(JSON.stringify({ action: 'uploadack', reqid: this.httprequest.uploadFileid }))); // Ask for more data
  170. return;
  171. }
  172. if (this.httprequest.state == 0) {
  173. // Check if this is a relay connection
  174. if (data == 'c') { this.httprequest.state = 1; sendConsoleText("Tunnel #" + this.httprequest.index + " now active", this.httprequest.sessionid); }
  175. } else {
  176. // Handle tunnel data
  177. if (this.httprequest.protocol == 0)
  178. {
  179. // Take a look at the protocol
  180. this.httprequest.protocol = parseInt(data);
  181. if (typeof this.httprequest.protocol != 'number') { this.httprequest.protocol = 0; }
  182. if (this.httprequest.protocol == 1)
  183. {
  184. // Remote terminal using native pipes
  185. if (process.platform == "win32")
  186. {
  187. this.httprequest._term = require('win-terminal').Start(80, 25);
  188. this.httprequest._term.pipe(this, { dataTypeSkip: 1 });
  189. this.pipe(this.httprequest._term, { dataTypeSkip: 1, end: false });
  190. this.prependListener('end', function () { this.httprequest._term.end(function () { sendConsoleText('Terminal was closed'); }); });
  191. }
  192. else
  193. {
  194. this.httprequest.process = childProcess.execFile("/bin/sh", ["sh"], { type: childProcess.SpawnTypes.TERM });
  195. this.httprequest.process.tunnel = this;
  196. this.httprequest.process.on('exit', function (ecode, sig) { this.tunnel.end(); });
  197. this.httprequest.process.stderr.on('data', function (chunk) { this.parent.tunnel.write(chunk); });
  198. this.httprequest.process.stdout.pipe(this, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text.
  199. this.pipe(this.httprequest.process.stdin, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text.
  200. this.prependListener('end', function () { this.httprequest.process.kill(); });
  201. }
  202. this.on('end', function () {
  203. if (process.platform == "win32")
  204. {
  205. // Unpipe the web socket
  206. this.unpipe(this.httprequest._term);
  207. this.httprequest._term.unpipe(this);
  208. // Clean up
  209. this.httprequest._term.end();
  210. this.httprequest._term = null;
  211. }
  212. });
  213. }
  214. }
  215. else if (this.httprequest.protocol == 5)
  216. {
  217. // Process files commands
  218. var cmd = null;
  219. try { cmd = JSON.parse(data); } catch (e) { };
  220. if (cmd == null) { return; }
  221. if ((cmd.ctrlChannel == '102938') || ((cmd.type == 'offer') && (cmd.sdp != null))) { return; } // If this is control data, handle it now.
  222. if (cmd.action == undefined) { return; }
  223. console.log('action: ', cmd.action);
  224. //sendConsoleText('CMD: ' + JSON.stringify(cmd));
  225. if ((cmd.path != null) && (process.platform != 'win32') && (cmd.path[0] != '/')) { cmd.path = '/' + cmd.path; } // Add '/' to paths on non-windows
  226. //console.log(objToString(cmd, 0, ' '));
  227. switch (cmd.action)
  228. {
  229. case 'ls':
  230. // Send the folder content to the browser
  231. var response = getDirectoryInfo(cmd.path);
  232. if (cmd.reqid != undefined) { response.reqid = cmd.reqid; }
  233. this.write(new Buffer(JSON.stringify(response)));
  234. break;
  235. case 'mkdir': {
  236. // Create a new empty folder
  237. fs.mkdirSync(cmd.path);
  238. break;
  239. }
  240. case 'mkfile': {
  241. // Create a new empty file
  242. fs.closeSync(fs.openSync(cmd.path, 'w'));
  243. break;
  244. }
  245. case 'rm': {
  246. // Delete, possibly recursive delete
  247. for (var i in cmd.delfiles)
  248. {
  249. try { deleteFolderRecursive(path.join(cmd.path, cmd.delfiles[i]), cmd.rec); } catch (e) { }
  250. }
  251. break;
  252. }
  253. case 'rename': {
  254. // Rename a file or folder
  255. var oldfullpath = path.join(cmd.path, cmd.oldname);
  256. var newfullpath = path.join(cmd.path, cmd.newname);
  257. try { fs.renameSync(oldfullpath, newfullpath); } catch (e) { console.log(e); }
  258. break;
  259. }
  260. case 'upload': {
  261. // Upload a file, browser to agent
  262. if (this.httprequest.uploadFile != undefined) { fs.closeSync(this.httprequest.uploadFile); this.httprequest.uploadFile = undefined; }
  263. if (cmd.path == undefined) break;
  264. var filepath = cmd.name ? path.join(cmd.path, cmd.name) : cmd.path;
  265. try { this.httprequest.uploadFile = fs.openSync(filepath, 'wbN'); } catch (e) { this.write(new Buffer(JSON.stringify({ action: 'uploaderror', reqid: cmd.reqid }))); break; }
  266. this.httprequest.uploadFileid = cmd.reqid;
  267. if (this.httprequest.uploadFile) { this.write(new Buffer(JSON.stringify({ action: 'uploadstart', reqid: this.httprequest.uploadFileid }))); }
  268. break;
  269. }
  270. case 'copy': {
  271. // Copy a bunch of files from scpath to dspath
  272. for (var i in cmd.names) {
  273. var sc = path.join(cmd.scpath, cmd.names[i]), ds = path.join(cmd.dspath, cmd.names[i]);
  274. if (sc != ds) { try { fs.copyFileSync(sc, ds); } catch (e) { } }
  275. }
  276. break;
  277. }
  278. case 'move': {
  279. // Move a bunch of files from scpath to dspath
  280. for (var i in cmd.names) {
  281. var sc = path.join(cmd.scpath, cmd.names[i]), ds = path.join(cmd.dspath, cmd.names[i]);
  282. if (sc != ds) { try { fs.copyFileSync(sc, ds); fs.unlinkSync(sc); } catch (e) { } }
  283. }
  284. break;
  285. }
  286. }
  287. }
  288. }
  289. });
  290. });
  291. tunnel.onerror = function (e) { sendConsoleText('ERROR: ' + JSON.stringify(e)); }
  292. tunnel.sessionid = data.sessionid;
  293. tunnel.rights = data.rights;
  294. tunnel.state = 0;
  295. tunnel.url = xurl;
  296. tunnel.protocol = 0;
  297. tunnel.tcpaddr = data.tcpaddr;
  298. tunnel.tcpport = data.tcpport;
  299. tunnel.end();
  300. // Put the tunnel in the tunnels list
  301. var index = nextTunnelIndex++;
  302. tunnel.index = index;
  303. tunnels[index] = tunnel;
  304. //sendConsoleText('New tunnel connection #' + index + ': ' + tunnel.url + ', rights: ' + tunnel.rights, data.sessionid);
  305. }
  306. }
  307. break;
  308. }
  309. default:
  310. // Unknown action, ignore it.
  311. break;
  312. }
  313. break;
  314. }
  315. default:
  316. // Unknown action, ignore it.
  317. break;
  318. }
  319. }
  320. });
  321. function processConsoleCommand(cmd, args, rights, sessionid)
  322. {
  323. try
  324. {
  325. var response = null;
  326. switch (cmd)
  327. {
  328. case 'help':
  329. response = 'Available commands are: osinfo, dbkeys, dbget, dbset, dbcompact, netinfo.';
  330. break;
  331. case 'osinfo': { // Return the operating system information
  332. var i = 1;
  333. if (args['_'].length > 0) { i = parseInt(args['_'][0]); if (i > 8) { i = 8; } response = 'Calling ' + i + ' times.'; }
  334. for (var j = 0; j < i; j++) {
  335. var pr = require('os').name();
  336. pr.sessionid = sessionid;
  337. pr.then(function (v) { sendConsoleText("OS: " + v, this.sessionid); });
  338. }
  339. break;
  340. }
  341. case 'dbkeys': { // Return all data store keys
  342. response = JSON.stringify(db.Keys);
  343. break;
  344. }
  345. case 'dbget': { // Return the data store value for a given key
  346. if (db == null) { response = 'Database not accessible.'; break; }
  347. if (args['_'].length != 1) {
  348. response = 'Proper usage: dbget (key)'; // Display the value for a given database key
  349. } else {
  350. response = db.Get(args['_'][0]);
  351. }
  352. break;
  353. }
  354. case 'dbset': { // Set a data store key and value pair
  355. if (db == null) { response = 'Database not accessible.'; break; }
  356. if (args['_'].length != 2) {
  357. response = 'Proper usage: dbset (key) (value)'; // Set a database key
  358. } else {
  359. var r = db.Put(args['_'][0], args['_'][1]);
  360. response = 'Key set: ' + r;
  361. }
  362. break;
  363. }
  364. case 'dbcompact': { // Compact the data store
  365. if (db == null) { response = 'Database not accessible.'; break; }
  366. var r = db.Compact();
  367. response = 'Database compacted: ' + r;
  368. break;
  369. }
  370. case 'tunnels': { // Show the list of current tunnels
  371. response = '';
  372. for (var i in tunnels) { response += 'Tunnel #' + i + ', ' + tunnels[i].url + '\r\n'; }
  373. if (response == '') { response = 'No websocket sessions.'; }
  374. break;
  375. }
  376. case 'netinfo': { // Show network interface information
  377. //response = objToString(mesh.NetInfo, 0, ' ');
  378. var interfaces = require('os').networkInterfaces();
  379. response = objToString(interfaces, 0, ' ', true);
  380. break;
  381. }
  382. default: { // This is an unknown command, return an error message
  383. response = 'Unknown command \"' + cmd + '\", type \"help\" for list of available commands.';
  384. break;
  385. }
  386. }
  387. } catch (e) { response = 'Command returned an exception error: ' + e; console.log(e); }
  388. if (response != null) { sendConsoleText(response, sessionid); }
  389. }
  390. // Get a formated response for a given directory path
  391. function getDirectoryInfo(reqpath)
  392. {
  393. var response = { path: reqpath, dir: [] };
  394. if (((reqpath == undefined) || (reqpath == '')) && (process.platform == 'win32')) {
  395. // List all the drives in the root, or the root itself
  396. var results = null;
  397. try { results = fs.readDrivesSync(); } catch (e) { } // TODO: Anyway to get drive total size and free space? Could draw a progress bar.
  398. if (results != null) {
  399. for (var i = 0; i < results.length; ++i) {
  400. var drive = { n: results[i].name, t: 1 };
  401. if (results[i].type == 'REMOVABLE') { drive.dt = 'removable'; } // TODO: See if this is USB/CDROM or something else, we can draw icons.
  402. response.dir.push(drive);
  403. }
  404. }
  405. } else {
  406. // List all the files and folders in this path
  407. if (reqpath == '') { reqpath = '/'; }
  408. var results = null, xpath = path.join(reqpath, '*');
  409. //if (process.platform == "win32") { xpath = xpath.split('/').join('\\'); }
  410. try { results = fs.readdirSync(xpath); } catch (e) { }
  411. if (results != null) {
  412. for (var i = 0; i < results.length; ++i) {
  413. if ((results[i] != '.') && (results[i] != '..')) {
  414. var stat = null, p = path.join(reqpath, results[i]);
  415. //if (process.platform == "win32") { p = p.split('/').join('\\'); }
  416. try { stat = fs.statSync(p); } catch (e) { } // TODO: Get file size/date
  417. if ((stat != null) && (stat != undefined)) {
  418. if (stat.isDirectory() == true) {
  419. response.dir.push({ n: results[i], t: 2, d: stat.mtime });
  420. } else {
  421. response.dir.push({ n: results[i], t: 3, s: stat.size, d: stat.mtime });
  422. }
  423. }
  424. }
  425. }
  426. }
  427. }
  428. return response;
  429. }
  430. // Delete a directory with a files and directories within it
  431. function deleteFolderRecursive(path, rec) {
  432. if (fs.existsSync(path)) {
  433. if (rec == true) {
  434. fs.readdirSync(path.join(path, '*')).forEach(function (file, index) {
  435. var curPath = path.join(path, file);
  436. if (fs.statSync(curPath).isDirectory()) { // recurse
  437. deleteFolderRecursive(curPath, true);
  438. } else { // delete file
  439. fs.unlinkSync(curPath);
  440. }
  441. });
  442. }
  443. fs.unlinkSync(path);
  444. }
  445. };