diff --git a/meshctrl.js b/meshctrl.js index f5d455ce..e17e3552 100644 --- a/meshctrl.js +++ b/meshctrl.js @@ -16,7 +16,7 @@ var settings = {}; const crypto = require('crypto'); const args = require('minimist')(process.argv.slice(2)); const path = require('path'); -const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage']; +const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay']; 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) { @@ -65,6 +65,7 @@ if (args['_'].length == 0) { console.log(" Shell - Access command shell of a remote device."); console.log(" Upload - Upload a file to a remote device."); console.log(" Download - Download a file from a remote device."); + console.log(" WebRelay - Creates a HTTP/HTTPS webrelay link for a remote device."); 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."); @@ -277,6 +278,12 @@ if (args['_'].length == 0) { else { ok = true; } break; } + case 'webrelay': { + if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); } + else if (args.type == null) { console.log(winRemoveSingleQuotes("Missing protocol type, use --type [http,https]")); } + else { ok = true; } + break; + } case 'deviceopenurl': { if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); } else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); } @@ -1015,6 +1022,21 @@ if (args['_'].length == 0) { console.log(" --target [localpath] - The local path to download the file to."); break; } + case 'webrelay': { + console.log("Generate a webrelay URL to access a HTTP/HTTPS service on a remote device, Example usages:\r\n"); + console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type http --port 80")); + console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type https --port 443")); + 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(" --type [http,https] - Type of relay from remote device, http or https."); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https."); + break; + } case 'deviceopenurl': { console.log("Open a web page on a remote device, Example usages:\r\n"); console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com")); @@ -1804,6 +1826,29 @@ function serverConnect() { req.end() break; } + case 'webrelay': { + var protocol = null; + if (args.type != null) { + if (args.type == 'http') { + protocol = 1; + } else if (args.type == 'https') { + protocol = 2; + } else { + console.log("Unknown protocol type: " + args.type); process.exit(1); + } + } + var port = null; + if (typeof args.port == 'number') { + if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); } + port = args.port; + } else if (protocol == 1) { + port = 80; + } else if (protocol == 2) { + port = 443; + } + ws.send(JSON.stringify({ action: 'webrelay', nodeid: args.id, port: port, appid: protocol, responseid: 'meshctrl' })); + break; + } case 'devicesharing': { if (args.add) { if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); } @@ -2208,6 +2253,7 @@ function serverConnect() { break; } case 'createDeviceShareLink': + case 'webrelay': if (data.result == 'OK') { if (data.publicid) { console.log('ID: ' + data.publicid); } console.log('URL: ' + data.url); diff --git a/meshuser.js b/meshuser.js index 90decaec..9655e0ba 100644 --- a/meshuser.js +++ b/meshuser.js @@ -2948,6 +2948,48 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'webrelay': + { + 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 + else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present + else { + if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; } + var snode = command.nodeid.split('/'); + if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; } + } + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: err })); } catch (ex) { } } + break; + } + // 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) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } } + return; + } + var relayid = null; + var addr = null; + if (node.mtype == 3) { // Setup device relay if needed + var mesh = parent.meshes[node.meshid]; + if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; } + } + var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req); + var webRelayPort = ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0)); + if (webRelayPort == 0) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'WebRelay Disabled' })); return; } catch (ex) { } } + const authRelayCookie = parent.parent.encodeCookie({ ruserid: user._id, x: req.session.x }, parent.parent.loginCookieEncryptionKey); + var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + command.nodeid + '&p=' + command.port + '&appid=' + command.appid + '&c=' + authRelayCookie; + if (addr != null) { url += '&addr=' + addr; } + if (relayid != null) { url += '&relayid=' + relayid } + command.url = url; + if (command.responseid != null) { command.result = 'OK'; } + try { ws.send(JSON.stringify(command)); } catch (ex) { } + }); + break; + } case 'runcommands': { if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's diff --git a/webserver.js b/webserver.js index 89d26f36..ffb6982e 100644 --- a/webserver.js +++ b/webserver.js @@ -8036,6 +8036,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF parent.debug('web', 'Invalid login, asking for email validation'); try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { } } else { + req.session.userid = user._id; + req.session.ip = req.clientIp; + setSessionRandom(req); func(ws, req, domain, user, null, authData); } } @@ -8051,6 +8054,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { } } else { // We are authenticated + req.session.userid = user._id; + req.session.ip = req.clientIp; + setSessionRandom(req); func(ws, req, domain, user); } } @@ -8179,7 +8185,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) { parent.debug('web', 'Invalid login, asking for email validation'); try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { } - } else { + } else { + req.session.userid = user._id; + req.session.ip = req.clientIp; + setSessionRandom(req); func(ws, req, domain, user); } }