From a98cdc1d5e63e5b33838611a74c8784cb97861cc Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Tue, 5 Jan 2021 17:02:24 -0800 Subject: [PATCH] Added device sharing link management support to MeshCtrl.js --- meshctrl.js | 117 ++++++++++++++++++++++++++++++++++++++- meshdesktopmultiplex.js | 2 +- meshuser.js | 39 ++++++++++--- views/default.handlebars | 8 ++- 4 files changed, 156 insertions(+), 10 deletions(-) diff --git a/meshctrl.js b/meshctrl.js index 27c90918..0f29f600 100644 --- a/meshctrl.js +++ b/meshctrl.js @@ -7,7 +7,7 @@ try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm var settings = {}; const crypto = require('crypto'); const args = require('minimist')(process.argv.slice(2)); -const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup']; +const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing']; 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; } } if (args['_'].length == 0) { @@ -53,6 +53,7 @@ if (args['_'].length == 0) { console.log(" DeviceOpenUrl - Open a URL on a remote device."); console.log(" DeviceMessage - Open a message box on a remote device."); console.log(" DeviceToast - Display a toast notification on a remote device."); + console.log(" DeviceSharing - View, add and remove sharing links for a given device."); console.log("\r\nSupported login arguments:"); console.log(" --url [wss://server] - Server url, wss://localhost:443 is default."); console.log(" - Use wss://localhost:443?key=xxx if login key is required."); @@ -205,6 +206,11 @@ if (args['_'].length == 0) { else { ok = true; } break; } + case 'devicesharing': { + if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); } + else { ok = true; } + break; + } case 'upload': { if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); } else if (args.file == null) { console.log("Local file missing, use --file [file] specify the file to upload"); } @@ -709,6 +715,30 @@ if (args['_'].length == 0) { console.log(" --powershell - Run a Windows PowerShell."); break; } + case 'devicesharing': { + var tzoffset = (new Date()).getTimezoneOffset() * 60000; // Offset in milliseconds + var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -5); + console.log("List sharing links for a specified device, Example usages:\r\n"); + console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid'")); + console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --remote abcdef")); + console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30")); + console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type terminal --consent prompt")); + console.log("\r\nRequired arguments:\r\n"); + if (process.platform == 'win32') { + console.log(" --id [deviceid] - The device identifier."); + } else { + console.log(" --id '[deviceid]' - The device identifier."); + } + console.log("\r\nOptional arguments:\r\n"); + console.log(" --remove [shareid] - Remove a device sharing link."); + console.log(" --add [guestname] - Add a device sharing link."); + console.log(" --type [desktop/terminal] - Type of sharing to add, default is desktop."); + console.log(" --consent [notify,prompt] - Consent flags, default is notify."); + console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now."); + console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time."); + console.log(" --duration [minutes] - Duration of the share, default is 60 minutes."); + break; + } case 'upload': { console.log("Upload a local file to a remote device, Example usages:\r\n"); console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target c:\\")); @@ -1287,6 +1317,54 @@ function serverConnect() { ws.send("{\"action\":\"authcookie\"}"); break; } + case 'devicesharing': { + if (args.add) { + if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); } + + // Sharing type, desktop or terminal + var p = 2; // Desktop + if (args.type != null) { + if (args.type.toLowerCase() == 'terminal') { p = 1; } + else if (args.type.toLowerCase() == 'desktop') { p = 2; } + else { console.log("Unknown type."); process.exit(1); return; } + } + + // User consent + var consent = 0; + if (args.consent == null) { + if (p == 1) { consent = 0x0002; } // Terminal notify + if (p == 2) { consent = 0x0001; } // Desktop notify + } else { + var flagStrs = args.consent.split(','); + for (var i in flagStrs) { + var flagStr = flagStrs[i].toLowerCase(); + if (flagStr == 'none') { consent = 0; } + else if (flagStr == 'notify') { + if (p == 1) { consent |= 0x0002; } // Terminal notify + if (p == 2) { consent |= 0x0001; } // Desktop notify + } else if (flagStr == 'prompt') { + if (p == 1) { consent |= 0x0010; } // Terminal prompt + if (p == 2) { consent |= 0x0008; } // Desktop prompt + } else if (flagStr == 'bar') { + if (p == 2) { consent |= 0x0040; } // Desktop toolbar + } else { console.log("Unknown consent type."); process.exit(1); return; } + } + } + + // Start and end time + var start = Math.floor(Date.now() / 1000), end = start + (60 * 60); + if (args.start) { start = Math.floor(Date.parse(args.start) / 1000); end = start + (60 * 60); } + if (args.end) { end = Math.floor(Date.parse(args.end) / 1000); if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; } } + if (args.duration) { end = start + parseInt(args.duration * 60); } + + ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, responseid: 'meshctrl' })); + } else if (args.remove) { + ws.send(JSON.stringify({ action: 'removeDeviceShare', nodeid: args.id, publicid: args.remove, responseid: 'meshctrl' })); + } else { + ws.send(JSON.stringify({ action: 'deviceShares', nodeid: args.id, responseid: 'meshctrl' })); + } + break; + } case 'deviceopenurl': { ws.send(JSON.stringify({ action: 'msg', type: 'openUrl', nodeid: args.id, url: args.openurl, responseid: 'meshctrl' })); break; @@ -1386,6 +1464,41 @@ function serverConnect() { } break; } + case 'deviceShares': { // DEVICESHARING + if (data.result != null) { + console.log(data.result); + } else { + if ((data.deviceShares == null) || (data.deviceShares.length == 0)) { + console.log('No device sharing links for this device.'); + } else { + for (var i in data.deviceShares) { + var share = data.deviceShares[i]; + var shareType = "Unknown"; + if (share.p == 1) { shareType = "Terminal"; } + if (share.p == 2) { shareType = "Desktop"; } + var consent = []; + if ((share.consent & 0x0001) != 0) { consent.push("Desktop Notify"); } + if ((share.consent & 0x0008) != 0) { consent.push("Desktop Prompt"); } + if ((share.consent & 0x0040) != 0) { consent.push("Desktop Connection Toolbar"); } + if ((share.consent & 0x0002) != 0) { consent.push("Terminal Notify"); } + if ((share.consent & 0x0010) != 0) { consent.push("Terminal Prompt"); } + if ((share.consent & 0x0004) != 0) { consent.push("Files Notify"); } + if ((share.consent & 0x0020) != 0) { consent.push("Files Prompt"); } + console.log('----------'); + console.log('Identifier: ' + share.publicid); + console.log('Type: ' + shareType); + console.log('UserId: ' + share.userid); + console.log('Guest Name: ' + share.guestName); + console.log('User Consent: ' + consent.join(', ')); + console.log('Start Time: ' + new Date(share.startTime).toLocaleString()); + console.log('Expire Time: ' + new Date(share.expireTime).toLocaleString()); + console.log('URL: ' + share.url); + } + } + } + process.exit(); + break; + } case 'userinfo': { // USERINFO if (settings.cmd == 'userinfo') { if (args.json) { @@ -1441,6 +1554,8 @@ function serverConnect() { case 'runcommands': case 'addusertousergroup': case 'removeuserfromusergroup': + case 'removeDeviceShare': + case 'createDeviceShareLink': case 'userbroadcast': { // BROADCAST if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) return; if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; } diff --git a/meshdesktopmultiplex.js b/meshdesktopmultiplex.js index 2a7a3983..1ee4bdf1 100644 --- a/meshdesktopmultiplex.js +++ b/meshdesktopmultiplex.js @@ -836,7 +836,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { // Do validation work if (cookie) { - if ((typeof cookie.expire == 'number') && (cookie.expire <= currentTime)) { delete req.query.nodeid; } + if ((typeof cookie.expire == 'number') && (cookie.expire <= Date.now())) { delete req.query.nodeid; } else if (typeof cookie.nid == 'string') { req.query.nodeid = cookie.nid; } } if ((req.query.nodeid == null) || (req.query.p != '2') || (req.query.id == null) || (domain == null)) { try { ws.close(); } catch (e) { } return; } // Not is not a valid remote desktop connection. diff --git a/meshuser.js b/meshuser.js index bcff2ca1..cc649295 100644 --- a/meshuser.js +++ b/meshuser.js @@ -4661,7 +4661,11 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'deviceShares': { var err = null; + + // Argument validation if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid + else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } + else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain // Handle any errors if (err != null) { @@ -4672,7 +4676,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Get the device rights parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) { // If node not found or we don't have remote control, reject. - if ((node == null) || ((rights & 8) == 0)) return; + if ((node == null) || ((rights & 8) == 0)) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } } + return; + } // If there is MESHRIGHT_DESKLIMITEDINPUT or MESHRIGHT_REMOTEVIEWONLY on this account, reject this request. if ((rights != 0xFFFFFFFF) && ((rights & 4352) != 0)) return; @@ -4705,8 +4712,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'removeDeviceShare': { var err = null; + + // Argument validation if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid - else if (common.validateString(command.publicid, 1, 128) == false) { err = 'Invalid public id'; } // Check the public identifier + else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } + else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain + if (common.validateString(command.publicid, 1, 128) == false) { err = 'Invalid public id'; } // Check the public identifier // Handle any errors if (err != null) { @@ -4717,8 +4728,11 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Get the device rights parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) { // If node not found or we don't have remote control, reject. - if ((node == null) || ((rights & 8) == 0)) return; - + if ((node == null) || ((rights & 8) == 0)) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } } + return; + } + // If there is MESHRIGHT_DESKLIMITEDINPUT or MESHRIGHT_REMOTEVIEWONLY on this account, reject this request. if ((rights != 0xFFFFFFFF) && ((rights & 4352) != 0)) return; @@ -4752,6 +4766,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (removed == true) { var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]); parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 }); + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'OK' })); } catch (ex) { } } + } else { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'Invalid device share identifier.' })); } catch (ex) { } } } }); }); @@ -4759,8 +4776,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'createDeviceShareLink': { var err = null; + + // Argument validation if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid - else if (common.validateString(command.guestname, 1, 128) == false) { err = 'Invalid guest name'; } // Check the guest name + else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } + else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain + if (common.validateString(command.guestname, 1, 128) == false) { err = 'Invalid guest name'; } // Check the guest name else if ((command.expire != null) && (typeof command.expire != 'number')) { err = 'Invalid expire time'; } // Check the expire time in hours else if ((command.start != null) && (typeof command.start != 'number')) { err = 'Invalid start time'; } // Check the start time in seconds else if ((command.end != null) && (typeof command.end != 'number')) { err = 'Invalid end time'; } // Check the end time in seconds @@ -4782,7 +4803,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Get the device rights parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) { // If node not found or we don't have remote control, reject. - if ((node == null) || ((rights & 8) == 0)) return; + if ((node == null) || ((rights & 8) == 0)) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createDeviceShareLink', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } } + return; + } // If there is MESHRIGHT_DESKLIMITEDINPUT or MESHRIGHT_REMOTEVIEWONLY on this account, reject this request. if ((rights != 0xFFFFFFFF) && ((rights & 4352) != 0)) return; @@ -4813,7 +4837,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + page + '?c=' + inviteCookie; if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; } command.url = url; - ws.send(JSON.stringify(command)); + if (command.responseid != null) { command.result = 'OK'; } + try { ws.send(JSON.stringify(command)); } catch (ex) { } // Create a device sharing database entry parent.db.Set({ _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: node._id, p: command.p, domain: node.domain, publicid: publicid, startTime: startTime, expireTime: expireTime, userid: user._id, guestName: command.guestname, consent: command.consent, url: url }); diff --git a/views/default.handlebars b/views/default.handlebars index d7b170ed..f1c45744 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -6156,7 +6156,12 @@ var dshare = deviceShares[i]; var trash = ' '; var details = format("{0}, {1} to {2}", ((dshare.p == 1)?"Terminal":"Desktop"), printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); - if (dshare.consent) { if (((dshare.consent & 8) != 0) || ((dshare.consent & 16) != 0)) { details += ", Prompt for consent"; } } + if (dshare.consent != null) { + if (dshare.consent == 0) { details += ", No Consent"; } else { + if (((dshare.consent & 8) != 0) || ((dshare.consent & 16) != 0)) { details += ", Prompt for consent"; } + if ((dshare.consent & 0x40) != 0) { details += ", Toolbar"; } + } + } x += '
 ' + dshare.guestName + '
' + trash + '
' + details + '
'; } x += ''; @@ -6352,6 +6357,7 @@ QE('idx_dlgOkButton', ok); } + function showShareDeviceEx(b, tag) { var consent = 0; if (currentNode.agent.caps & 1) {