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