Added file upload/download to MeshCtrl.js
Ylian Saint-Hilaire committed
Jul 16, 2020 at 14:08 UTC
69f5fcb8976dc9813095e7781c4827b41bc37550
1 file changed
+154
-33
meshctrl.js
+154
-33
@@ -7,7 +7,7 @@ try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm
7
var settings = {};
8
const crypto = require('crypto');
9
const args = require('minimist')(process.argv.slice(2));
10
-const possibleCommands = ['listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell'];
10
+const possibleCommands = ['listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download'];
11
if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
12
13
if (args['_'].length == 0) {
@@ -43,6 +43,8 @@ if (args['_'].length == 0) {
43
console.log(" ShowEvents - Display real-time server events in JSON format.");
44
console.log(" RunCommand - Run a shell command on a remote device.");
45
console.log(" Shell - Access command shell of a remote device.");
46
+ console.log(" Upload - Upload a file to a remote device.");
47
+ console.log(" Download - Download a file from a remote device.");
48
console.log("\r\nSupported login arguments:");
49
console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
50
console.log(" --loginuser [username] - Login username, admin is default.");
@@ -171,6 +173,21 @@ if (args['_'].length == 0) {
173
else { ok = true; }
174
break;
175
}
176
+ case 'upload': {
177
+ if (args.id == null) { console.log("Missing device id, use --id [deviceid]"); }
178
+ else if (args.file == null) { console.log("Local file missing, use --file [file] specify the file to upload"); }
179
+ else if (args.target == null) { console.log("Remote target path missing, use --target [path] to specify the remote location"); }
180
+ else if (require('fs').existsSync(args.file) == false) { console.log("Local file does not exists, check --file"); }
181
+ else { ok = true; }
182
+ break;
183
+ }
184
+ case 'download': {
185
+ if (args.id == null) { console.log("Missing device id, use --id [deviceid]"); }
186
+ else if (args.file == null) { console.log("Remote file missing, use --file [file] specify the remote file to download"); }
187
+ else if (args.target == null) { console.log("Target path missing, use --target [path] to specify the local download location"); }
188
+ else { ok = true; }
189
+ break;
190
+ }
191
case 'help': {
192
if (args['_'].length < 2) {
193
console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
@@ -445,6 +462,27 @@ if (args['_'].length == 0) {
462
console.log(" --powershell - Run a Windows PowerShell.");
463
break;
464
}
465
+ case 'upload': {
466
+ console.log("Upload a local file to a remote device, Example usages:\r\n");
467
+ console.log(" MeshCtrl Upload --id deviceid --file sample.txt --target c:\\");
468
+ console.log(" MeshCtrl Upload --id deviceid --file sample.txt --target /tmp");
469
+ console.log("\r\nRequired arguments:\r\n");
470
+ console.log(" --id [deviceid] - The device identifier.");
471
+ console.log(" --file [localfile] - The local file to upload.");
472
+ console.log(" --target [remotepath] - The remote path to upload the file to.");
473
+ break;
474
+ }
475
+ case 'download': {
476
+ console.log("Download a file from a remote device, Example usages:\r\n");
477
+ console.log(" MeshCtrl Download --id deviceid --file C:\\sample.txt --target c:\\temp");
478
+ console.log(" MeshCtrl Download --id deviceid --file /tmp/sample.txt --target /tmp");
479
+ console.log("\r\nRequired arguments:\r\n");
480
+ console.log(" --id [deviceid] - The device identifier.");
481
+ console.log(" --file [remotefile] - The remote file to download.");
482
+ console.log("\r\nOptional arguments:\r\n");
483
+ console.log(" --target [localpath] - The local path to download the file to.");
484
+ break;
485
+ }
486
default: {
487
console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
488
}
@@ -807,7 +845,9 @@ function serverConnect() {
845
ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl' }));
846
break;
847
}
810
- case 'shell': {
848
+ case 'shell':
849
+ case 'upload':
850
+ case 'download': {
851
ws.send("{\"action\":\"authcookie\"}");
852
break;
853
}
@@ -843,12 +883,14 @@ function serverConnect() {
883
}
884
break;
885
}
846
- case 'authcookie': { // SHELL
847
- if (settings.cmd == 'shell') {
886
+ case 'authcookie': { // SHELL, UPLOAD, DOWNLOAD
887
+ if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) {
888
+ var protocol = 1; // Terminal
889
+ if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files
890
if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
891
var id = getRandomHex(6);
850
- ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=1&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
851
- connectShell(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=1&nodeid=' + args.id + '&id=' + id + '&auth=' + data.cookie));
892
+ ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
893
+ connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&auth=' + data.cookie));
894
}
895
break;
896
}
@@ -1022,7 +1064,7 @@ function serverConnect() {
1064
for (var i in data.meshes) {
1065
const m = data.meshes[i];
1066
var mid = m._id.split('/')[2];
1025
- if (args.hex) { mid = Buffer.from(mid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); }
1067
+ if (args.hex) { mid = '0x' + Buffer.from(mid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); }
1068
var t = "\"" + mid + "\", \"" + m.name + "\"";
1069
console.log(t);
1070
}
@@ -1089,8 +1131,8 @@ function serverConnect() {
1131
});
1132
}
1133
1092
-// Connect tunnel to a remote agent shell
1093
-function connectShell(url) {
1134
+// Connect tunnel to a remote agent
1135
+function connectTunnel(url) {
1136
// Setup WebSocket options
1137
var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
1138
@@ -1105,36 +1147,115 @@ function connectShell(url) {
1147
settings.tunnelws.on('open', function () { console.log('Waiting for Agent...'); }); // Wait for agent connection
1148
settings.tunnelws.on('close', function () { console.log('Connection Closed.'); process.exit(); });
1149
settings.tunnelws.on('error', function (err) { console.log(err); process.exit(); });
1108
- settings.tunnelws.on('message', function (rawdata) {
1109
- var data = rawdata.toString();
1110
- if (settings.tunnelwsstate == 1) {
1111
- process.stdout.write(data);
1112
- } else if (settings.tunnelwsstate == 0) {
1113
- if (data == 'c') {
1150
+
1151
+ if (settings.cmd == 'shell') {
1152
+ // This code does all of the work for a shell command
1153
+ settings.tunnelws.on('message', function (rawdata) {
1154
+ var data = rawdata.toString();
1155
+ if (settings.tunnelwsstate == 1) {
1156
+ process.stdout.write(data);
1157
+ } else if (settings.tunnelwsstate == 0) {
1158
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
1159
// Send terminal size
1160
var termSize = null;
1161
if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
1162
if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); }
1118
- console.log('Connected.');
1163
settings.tunnelwsstate = 1;
1120
- settings.tunnelws.send('1');
1164
+ settings.tunnelws.send('1'); // Terminal
1165
+ process.stdin.setEncoding('utf8');
1166
+ process.stdin.setRawMode(true);
1167
+ process.stdout.setEncoding('utf8');
1168
+ process.stdin.unpipe(process.stdout);
1169
+ process.stdout.unpipe(process.stdin);
1170
+ process.stdin.on('data', function (data) { settings.tunnelws.send(Buffer.from(data)); });
1171
+ //process.stdin.on('readable', function () { var chunk; while ((chunk = process.stdin.read()) !== null) { settings.tunnelws.send(Buffer.from(chunk)); } });
1172
+ process.stdin.on('end', function () { process.exit(); });
1173
+ process.stdout.on('resize', function () {
1174
+ var termSize = null;
1175
+ if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
1176
+ if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: termSize[0], rows: termSize[1] })); }
1177
+ });
1178
}
1122
- else if (data == 'cr') { console.log('Connected, session is being recorded.'); settings.tunnelwsstate = 1; settings.tunnelws.send('1'); }
1123
- process.stdin.setEncoding('utf8');
1124
- process.stdin.setRawMode(true);
1125
- process.stdout.setEncoding('utf8');
1126
- process.stdin.unpipe(process.stdout);
1127
- process.stdout.unpipe(process.stdin);
1128
- process.stdin.on('data', function (data) { settings.tunnelws.send(Buffer.from(data)); });
1129
- //process.stdin.on('readable', function () { var chunk; while ((chunk = process.stdin.read()) !== null) { settings.tunnelws.send(Buffer.from(chunk)); } });
1130
- process.stdin.on('end', function () { process.exit(); });
1131
- process.stdout.on('resize', function() {
1132
- var termSize = null;
1133
- if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
1134
- if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: termSize[0], rows: termSize[1] })); }
1135
- });
1136
- }
1137
- });
1179
+ });
1180
+ } else if (settings.cmd == 'upload') {
1181
+ // This code does all of the work for a file upload
1182
+ // node meshctrl upload --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file readme.md --target c:\
1183
+ settings.tunnelws.on('message', function (rawdata) {
1184
+ if (settings.tunnelwsstate == 1) {
1185
+ var cmd = null;
1186
+ try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
1187
+ if (cmd.reqid == 'up') {
1188
+ if ((cmd.action == 'uploadack') || (cmd.action == 'uploadstart')) {
1189
+ var buf = Buffer.alloc(4096);
1190
+ var len = require('fs').readSync(settings.uploadFile, buf, 0, 4096, settings.uploadPtr);
1191
+ settings.uploadPtr += len;
1192
+ if (len > 0) {
1193
+ settings.tunnelws.send(buf.slice(0, len));
1194
+ } else {
1195
+ console.log('Upload done, ' + settings.uploadPtr + ' bytes sent.');
1196
+ if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); }
1197
+ process.exit();
1198
+ }
1199
+ } else if (cmd.action == 'uploaderror') {
1200
+ if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); }
1201
+ console.log('Upload error.');
1202
+ process.exit();
1203
+ }
1204
+ }
1205
+ } else if (settings.tunnelwsstate == 0) {
1206
+ var data = rawdata.toString();
1207
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
1208
+ settings.tunnelwsstate = 1;
1209
+ settings.tunnelws.send('5'); // Files
1210
+ settings.uploadSize = require('fs').statSync(args.file).size;
1211
+ settings.uploadFile = require('fs').openSync(args.file, 'r');
1212
+ settings.uploadPtr = 0;
1213
+ settings.tunnelws.send(JSON.stringify({ action: 'upload', reqid: 'up', path: require('path').dirname(args.target), name: require('path').basename(args.file), size: settings.uploadSize }));
1214
+ }
1215
+ });
1216
+ } else if (settings.cmd == 'download') {
1217
+ // This code does all of the work for a file download
1218
+ // node meshctrl download --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file c:\temp\MC-8Languages.png --target c:\temp\bob.png
1219
+ settings.tunnelws.on('message', function (rawdata) {
1220
+ if (settings.tunnelwsstate == 1) {
1221
+ if ((rawdata.length > 0) && (rawdata[0] != '{')) {
1222
+ // This is binary data, this test is ok because 4 first bytes is a control value.
1223
+ if ((rawdata.length > 4) && (settings.downloadFile != null)) { settings.downloadSize += (rawdata.length - 4); require('fs').writeSync(settings.downloadFile, rawdata, 4, rawdata.length - 4); }
1224
+ if ((rawdata[3] & 1) != 0) { // Check end flag
1225
+ // File is done, close everything.
1226
+ if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
1227
+ console.log('Download completed, ' + settings.downloadSize + ' bytes written.');
1228
+ process.exit();
1229
+ } else {
1230
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'ack', id: args.file })); // Send the ACK
1231
+ }
1232
+ } else {
1233
+ // This is text data
1234
+ var cmd = null;
1235
+ try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
1236
+ if (cmd.action == 'download') {
1237
+ if (cmd.id != args.file) return;
1238
+ if (cmd.sub == 'start') {
1239
+ settings.downloadFile = require('fs').openSync(args.target, 'w');
1240
+ settings.downloadSize = 0;
1241
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'startack', id: args.file }));
1242
+ console.log('Download started...');
1243
+ } else if (cmd.sub == 'cancel') {
1244
+ if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
1245
+ console.log('Download canceled.');
1246
+ process.exit();
1247
+ }
1248
+ }
1249
+ }
1250
+ } else if (settings.tunnelwsstate == 0) {
1251
+ var data = rawdata.toString();
1252
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
1253
+ settings.tunnelwsstate = 1;
1254
+ settings.tunnelws.send('5'); // Files
1255
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'start', id: args.file, path: args.file }));
1256
+ }
1257
+ });
1258
+ }
1259
}
1260
1261
// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)