diff --git a/agents/MeshCmd-signed.exe b/agents/MeshCmd-signed.exe index 8923c07a..e3e4df3d 100644 Binary files a/agents/MeshCmd-signed.exe and b/agents/MeshCmd-signed.exe differ diff --git a/agents/MeshCmd64-signed.exe b/agents/MeshCmd64-signed.exe index 6469acb4..642df48c 100644 Binary files a/agents/MeshCmd64-signed.exe and b/agents/MeshCmd64-signed.exe differ diff --git a/agents/meshcore.js b/agents/meshcore.js index 9d3cd688..d342653e 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -23,8 +23,7 @@ if (process.platform == 'win32' && require('user-sessions').getDomain == null) { }; } -// NOTE: This seems to cause big problems, don't enable the debugger in the server's meshcore. -//attachDebugger({ webport: 9999, wait: 1 }).then(function (prt) { console.log('Point Browser for Debug to port: ' + prt); }); +var promise = require('promise'); // Mesh Rights var MNG_ERROR = 65; @@ -1043,6 +1042,35 @@ function server_check_consentTimer(id) { return false; } +function tunnel_finalized() +{ + console.info1('Tunnel Request Finalized'); +} +function tunnel_checkServerIdentity(certs) +{ + /* + try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); } + */ + + // Check if this is an old agent, no certificate checks are possible in this situation. Display a warning. + if ((require('MeshAgent').ServerInfo == null) || (require('MeshAgent').ServerInfo.ControlChannelCertificate == null) || (certs[0].digest == null)) { sendAgentMessage("This agent is using insecure tunnels, consider updating.", 3, 119, true); return; } + + // If the tunnel certificate matches the control channel certificate, accept the connection + if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; // Control channel certificate matches using full cert hash + if ((certs[0].fingerprint != null) && (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint)) return; // Control channel certificate matches using public key hash + + // Check that the certificate is the one expected by the server, fail if not. + if ((tunnel_checkServerIdentity.servertlshash != null) && (tunnel_checkServerIdentity.servertlshash.toLowerCase() != certs[0].digest.split(':').join('').toLowerCase())) { throw new Error('BadCert') } +} + +function tunnel_onError() +{ + sendConsoleText("ERROR: Unable to connect relay tunnel to: " + this.url + ", " + JSON.stringify(e)); +} + // Handle a mesh agent command function handleServerCommand(data) { if (typeof data == 'object') { @@ -1062,7 +1090,8 @@ function handleServerCommand(data) { } break; } - case 'tunnel': { + case 'tunnel': + { if (data.value != null) { // Process a new tunnel connection request // Create a new tunnel object var xurl = getServerTargetUrlEx(data.value); @@ -1074,31 +1103,15 @@ function handleServerCommand(data) { // Perform manual server TLS certificate checking based on the certificate hash given by the server. woptions.rejectUnauthorized = 0; - woptions.checkServerIdentity = function checkServerIdentity(certs) { - /* - try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); } - try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); } - try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); } - try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); } - */ - - // Check if this is an old agent, no certificate checks are possible in this situation. Display a warning. - if ((require('MeshAgent').ServerInfo == null) || (require('MeshAgent').ServerInfo.ControlChannelCertificate == null) || (certs[0].digest == null)) { sendAgentMessage("This agent is using insecure tunnels, consider updating.", 3, 119, true); return; } - - // If the tunnel certificate matches the control channel certificate, accept the connection - if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; // Control channel certificate matches using full cert hash - if ((certs[0].fingerprint != null) && (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint)) return; // Control channel certificate matches using public key hash - - // Check that the certificate is the one expected by the server, fail if not. - if ((checkServerIdentity.servertlshash != null) && (checkServerIdentity.servertlshash.toLowerCase() != certs[0].digest.split(':').join('').toLowerCase())) { throw new Error('BadCert') } - } + woptions.checkServerIdentity = tunnel_checkServerIdentity; woptions.checkServerIdentity.servertlshash = data.servertlshash; //sendConsoleText(JSON.stringify(woptions)); //sendConsoleText('TUNNEL: ' + JSON.stringify(data, null, 2)); + var tunnel = http.request(woptions); tunnel.upgrade = onTunnelUpgrade; - tunnel.on('error', function (e) { sendConsoleText("ERROR: Unable to connect relay tunnel to: " + this.url + ", " + JSON.stringify(e)); }); + tunnel.on('error', tunnel_onError); tunnel.sessionid = data.sessionid; tunnel.rights = data.rights; tunnel.consent = data.consent; @@ -1122,11 +1135,13 @@ function handleServerCommand(data) { tunnel.tcpport = data.tcpport; tunnel.udpaddr = data.udpaddr; tunnel.udpport = data.udpport; - tunnel.end(); + // Put the tunnel in the tunnels list var index = nextTunnelIndex++; tunnel.index = index; tunnels[index] = tunnel; + tunnel.once('~', tunnel_finalized); + tunnel.end(); //sendConsoleText('New tunnel connection #' + index + ': ' + tunnel.url + ', rights: ' + tunnel.rights, data.sessionid); } @@ -1355,7 +1370,9 @@ function handleServerCommand(data) { this._dispatcher.on('connection', function (c) { this._c = c; this._c.root = this.parent; - this._c.on('end', function () { + this._c.on('end', function () + { + this.root._dispatcher.close(); this.root._dispatcher = null; this.root = null; mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true }); @@ -1840,20 +1857,34 @@ function getDirectoryInfo(reqpath) { return response; } +function tunnel_s_finalized() +{ + console.info1('Tunnel Socket Finalized'); +} + + +function tunnel_onIdleTimeout() +{ + this.ping(); + this.setTimeout(require('MeshAgent').idleTimeout * 1000); +} + // Tunnel callback operations -function onTunnelUpgrade(response, s, head) { +function onTunnelUpgrade(response, s, head) +{ + this.s = s; + s.once('~', tunnel_s_finalized); s.httprequest = this; s.end = onTunnelClosed; s.tunnel = this; s.descriptorMetadata = "MeshAgent_relayTunnel"; - if (require('MeshAgent').idleTimeout != null) { + + if (require('MeshAgent').idleTimeout != null) + { s.setTimeout(require('MeshAgent').idleTimeout * 1000); - s.on('timeout', function () { - this.ping(); - this.setTimeout(require('MeshAgent').idleTimeout * 1000); - }); + s.on('timeout', tunnel_onIdleTimeout); } //sendConsoleText('onTunnelUpgrade - ' + this.tcpport + ' - ' + this.udpport); @@ -1937,7 +1968,25 @@ function onTcpRelayServerTunnelData(data) { } } -function onTunnelClosed() { +function onTunnelClosed() +{ + if (this.httprequest._dispatcher != null && this.httprequest.term == null) + { + // Windows Dispatcher was created to spawn a child connection, but the child didn't connect yet, so we have to shutdown the dispatcher, otherwise the child may end up hanging + if (this.httprequest._dispatcher.close) { this.httprequest._dispatcher.close(); } + this.httprequest._dispatcher = null; + } + + if (this.tunnel) + { + if (tunnels[this.httprequest.index] == null) + { + this.tunnel.s = null; + this.tunnel = null; + return; + } + } + var tunnel = tunnels[this.httprequest.index]; if (tunnel == null) return; // Stop duplicate calls. @@ -1987,7 +2036,7 @@ function onTunnelClosed() { } catch (ex) { } //sendConsoleText("Tunnel #" + this.httprequest.index + " closed. Sent -> " + this.bytesSent_uncompressed + ' bytes (uncompressed), ' + this.bytesSent_actual + ' bytes (actual), ' + this.bytesSent_ratio + '% compression', this.httprequest.sessionid); - if (this.httprequest.index) { delete tunnels[this.httprequest.index]; } + /* // Close the watcher if required @@ -2014,11 +2063,523 @@ function onTunnelClosed() { } // Clean up WebSocket + delete tunnels[this.httprequest.index]; + tunnel = null; + this.tunnel.s = null; + this.tunnel = null; this.removeAllListeners('data'); } function onTunnelSendOk() { /*sendConsoleText("Tunnel #" + this.index + " SendOK.", this.sessionid);*/ } -function onTunnelData(data) { - //console.log("OnTunnelData"); + +function terminal_onconnection (c) +{ + if (this.httprequest.connectionPromise.completed) + { + c.end(); + } + else + { + this.httprequest.connectionPromise._res(c); + } +} +function terminal_user_onconnection(c) +{ + console.info1('completed-2: ' + this.connectionPromise.completed); + + if (this.connectionPromise.completed) + { + c.end(); + } + else + { + this.connectionPromise._res(c); + } +} +function terminal_stderr_ondata(c) +{ + this.stdout.write(c); +} +function terminal_onend() +{ + this.httprequest.process.kill(); +} + +function terminal_onexit() +{ + this.tunnel.end(); +} +function terminal_onfinalized() +{ + this.httprequest = null; + console.info1('Dispatcher Finalized'); +} +function terminal_end() +{ + if (this.httprequest == null) { return; } + if (this.httprequest.tpromise._consent) { this.httprequest.tpromise._consent.close(); } + if (this.httprequest.connectionPromise) { this.httprequest.connectionPromise._rej('Closed'); } + + // Remove the terminal session to the count to update the server + if (this.httprequest.userid != null) + { + var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest); + if (tunnelUserCount.terminal[userid] != null) { tunnelUserCount.terminal[userid]--; if (tunnelUserCount.terminal[userid] <= 0) { delete tunnelUserCount.terminal[userid]; } } + try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { } + broadcastSessionsToRegisteredApps(); + } + + if (process.platform == 'win32') + { + // Unpipe the web socket + this.unpipe(this.httprequest._term); + if (this.httprequest._term) { this.httprequest._term.unpipe(this); } + + // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends). + if (this.rtcchannel) + { + this.rtcchannel.unpipe(this.httprequest._term); + if (this.httprequest._term) { this.httprequest._term.unpipe(this.rtcchannel); } + } + + // Clean up + if (this.httprequest._term) { this.httprequest._term.end(); } + this.httprequest._term = null; + this.httprequest._dispatcher = null; + } + + this.httprequest = null; + +} + +function terminal_promise_connection_rejected(e) +{ + // FAILED to connect terminal + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.ws.end(); +} + +function terminal_promise_connection_resolved(term) +{ + this._internal.completedArgs = []; + + // SUCCESS + var stdoutstream; + var stdinstream; + if (process.platform == 'win32') + { + this.ws.httprequest._term = term; + this.ws.httprequest._term.tunnel = this.ws; + stdoutstream = stdinstream = term; + } + else + { + term.descriptorMetadata = 'Remote Terminal'; + this.ws.httprequest.process = term; + this.ws.httprequest.process.tunnel = this.ws; + term.stderr.stdout = term.stdout; + term.stderr.on('data', terminal_stderr_ondata); + stdoutstream = term.stdout; + stdinstream = term.stdin; + this.ws.prependListener('end', terminal_onend); + term.prependListener('exit', terminal_onexit); + } + + this.ws.removeAllListeners('data'); + this.ws.on('data', onTunnelControlData); + + stdoutstream.pipe(this.ws, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text. + this.ws.pipe(stdinstream, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text. + + // Add the terminal session to the count to update the server + if (this.ws.httprequest.userid != null) + { + var userid = getUserIdAndGuestNameFromHttpRequest(this.ws.httprequest); + if (tunnelUserCount.terminal[userid] == null) { tunnelUserCount.terminal[userid] = 1; } else { tunnelUserCount.terminal[userid]++; } + try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { } + broadcastSessionsToRegisteredApps(); + } + + // Toast Notification, if required + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 2)) + { + // User Notifications is required + var notifyMessage = currentTranslation['terminalNotify'].replace('{0}', this.ws.httprequest.username); + var notifyTitle = "MeshCentral"; + if (this.ws.httprequest.soptions != null) + { + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } + if (this.ws.httprequest.soptions.notifyMsgTerminal != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgTerminal.replace('{0}', this.ws.httprequest.realname).replace('{1}', this.ws.httprequest.username); } + } + try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { } + } + this.ws = null; +} +function terminal_promise_consent_rejected(e) +{ + // DO NOT start terminal + this.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.that.end(); + + this.that = null; + this.httprequest = null; +} +function promise_init(res, rej) { this._res = res; this._rej = rej; } +function terminal_userpromise_resolved(u) +{ + + var that = this.that; + if (u.Active.length > 0) + { + var tmp; + var username = '"' + u.Active[0].Domain + '\\' + u.Active[0].Username + '"'; + + + if (require('win-virtual-terminal').supported) + { + // ConPTY PseudoTerminal + tmp = require('win-dispatcher').dispatch({ user: username, modules: [{ name: 'win-virtual-terminal', script: getJSModule('win-virtual-terminal') }], launch: { module: 'win-virtual-terminal', method: (that.httprequest.protocol == 9 ? 'StartPowerShell' : 'Start'), args: [this.cols, this.rows] } }); + } + else + { + // Legacy Terminal + tmp = require('win-dispatcher').dispatch({ user: username, modules: [{ name: 'win-terminal', script: getJSModule('win-terminal') }], launch: { module: 'win-terminal', method: (that.httprequest.protocol == 9 ? 'StartPowerShell' : 'Start'), args: [this.cols, this.rows] } }); + } + that.httprequest._dispatcher = tmp; + that.httprequest._dispatcher.connectionPromise = that.httprequest.connectionPromise; + that.httprequest._dispatcher.on('connection', terminal_user_onconnection); + that.httprequest._dispatcher.on('~', terminal_onfinalized); + } + this.that = null; + that = null; +} + +function terminal_promise_consent_resolved() +{ + this.httprequest.connectionPromise = new promise(promise_init); + this.httprequest.connectionPromise.ws = this.that; + + // Start Terminal + if (process.platform == 'win32') + { + try + { + var cols = 80, rows = 25; + if (this.httprequest.xoptions) + { + if (this.httprequest.xoptions.rows) { rows = this.httprequest.xoptions.rows; } + if (this.httprequest.xoptions.cols) { cols = this.httprequest.xoptions.cols; } + } + + if ((this.httprequest.protocol == 1) || (this.httprequest.protocol == 6)) + { + // Admin Terminal + if (require('win-virtual-terminal').supported) + { + // ConPTY PseudoTerminal + // this.httprequest._term = require('win-virtual-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](80, 25); + + // The above line is commented out, because there is a bug with ClosePseudoConsole() API, so this is the workaround + this.httprequest._dispatcher = require('win-dispatcher').dispatch({ modules: [{ name: 'win-virtual-terminal', script: getJSModule('win-virtual-terminal') }], launch: { module: 'win-virtual-terminal', method: (this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'), args: [cols, rows] } }); + this.httprequest._dispatcher.httprequest = this.httprequest; + this.httprequest._dispatcher.on('connection', terminal_onconnection); + this.httprequest._dispatcher.on('~', terminal_onfinalized); + } + else + { + // Legacy Terminal + this.httprequest.connectionPromise._res(require('win-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](cols, rows)); + } + } + else + { + // Logged in user + var userPromise = require('user-sessions').enumerateUsers(); + userPromise.that = this; + userPromise.cols = cols; + userPromise.rows = rows; + userPromise.then(terminal_userpromise_resolved); + } + } catch (ex) + { + this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString()); + } + } + else + { + try + { + var bash = fs.existsSync('/bin/bash') ? '/bin/bash' : false; + var sh = fs.existsSync('/bin/sh') ? '/bin/sh' : false; + var login = process.platform == 'linux' ? '/bin/login' : '/usr/bin/login'; + + var env = { HISTCONTROL: 'ignoreboth' }; + if (process.env['LANG']) { env['LANG'] = process.env['LANG']; } + if (process.env['PATH']) { env['PATH'] = process.env['PATH']; } + if (this.httprequest.xoptions) + { + if (this.httprequest.xoptions.rows) { env.LINES = ('' + this.httprequest.xoptions.rows); } + if (this.httprequest.xoptions.cols) { env.COLUMNS = ('' + this.httprequest.xoptions.cols); } + } + var options = { type: childProcess.SpawnTypes.TERM, uid: (this.httprequest.protocol == 8) ? require('user-sessions').consoleUid() : null, env: env }; + if (this.httprequest.xoptions && this.httprequest.xoptions.requireLogin) + { + if (!require('fs').existsSync(login)) { throw ('Unable to spawn login process'); } + this.httprequest.connectionPromise._res(childProcess.execFile(login, ['login'], options)); // Start login shell + } + else if (bash) + { + var p = childProcess.execFile(bash, ['bash'], options); // Start bash + // Spaces at the beginning of lines are needed to hide commands from the command history + if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string')) + { + if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); } + } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); } + this.httprequest.connectionPromise._res(p); + } + else if (sh) + { + var p = childProcess.execFile(sh, ['sh'], options); // Start sh + // Spaces at the beginning of lines are needed to hide commands from the command history + if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string')) + { + if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); } + } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); } + this.httprequest.connectionPromise._res(p); + } + else + { + this.httprequest.connectionPromise._rej('Failed to start remote terminal session, no shell found'); + } + } catch (ex) + { + this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString()); + } + } + + this.httprequest.connectionPromise.then(terminal_promise_connection_resolved, terminal_promise_connection_rejected); + this.that = null; + this.httprequest = null; +} +function tunnel_kvm_end() +{ + --this.desktop.kvm.connectionCount; + + // Remove ourself from the list of remote desktop session + var i = this.desktop.kvm.tunnels.indexOf(this); + if (i >= 0) { this.desktop.kvm.tunnels.splice(i, 1); } + + // Send a metadata update to all desktop sessions + var users = {}; + if (this.httprequest.desktop.kvm.tunnels != null) + { + for (var i in this.httprequest.desktop.kvm.tunnels) + { + try + { + var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest.desktop.kvm.tunnels[i].httprequest); + if (users[userid] == null) { users[userid] = 1; } else { users[userid]++; } + } catch (ex) { sendConsoleText(ex); } + } + for (var i in this.httprequest.desktop.kvm.tunnels) + { + try { this.httprequest.desktop.kvm.tunnels[i].write(JSON.stringify({ ctrlChannel: '102938', type: 'metadata', users: users })); } catch (ex) { } + } + tunnelUserCount.desktop = users; + try { mesh.SendCommand({ action: 'sessions', type: 'kvm', value: users }); } catch (ex) { } + broadcastSessionsToRegisteredApps(); + } + + // Unpipe the web socket + try + { + this.unpipe(this.httprequest.desktop.kvm); + this.httprequest.desktop.kvm.unpipe(this); + } catch (ex) { } + + // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends). + if (this.rtcchannel) + { + try + { + this.rtcchannel.unpipe(this.httprequest.desktop.kvm); + this.httprequest.desktop.kvm.unpipe(this.rtcchannel); + } + catch (ex) { } + } + + // Place wallpaper back if needed + // TODO + + if (this.desktop.kvm.connectionCount == 0) + { + // Display a toast message. This may not be supported on all platforms. + // try { require('toaster').Toast('MeshCentral', 'Remote Desktop Control Ended.'); } catch (ex) { } + + this.httprequest.desktop.kvm.end(); + if (this.httprequest.desktop.kvm.connectionBar) + { + this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); + this.httprequest.desktop.kvm.connectionBar.close(); + this.httprequest.desktop.kvm.connectionBar = null; + } + } else + { + for (var i in this.httprequest.desktop.kvm.users) + { + if ((this.httprequest.desktop.kvm.users[i] == this.httprequest.username) && this.httprequest.desktop.kvm.connectionBar) + { + for (var j in this.httprequest.desktop.kvm.rusers) { if (this.httprequest.desktop.kvm.rusers[j] == this.httprequest.realname) { this.httprequest.desktop.kvm.rusers.splice(j, 1); break; } } + this.httprequest.desktop.kvm.users.splice(i, 1); + this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); + this.httprequest.desktop.kvm.connectionBar.close(); + this.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(this.httprequest.privacybartext.replace('{0}', this.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', this.httprequest.desktop.kvm.users.join(', ')), require('MeshAgent')._tsid, color_options); + this.httprequest.desktop.kvm.connectionBar.httprequest = this.httprequest; + this.httprequest.desktop.kvm.connectionBar.on('close', function () + { + MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest); + for (var i in this.httprequest.desktop.kvm._pipedStreams) + { + this.httprequest.desktop.kvm._pipedStreams[i].end(); + } + this.httprequest.desktop.kvm.end(); + }); + break; + } + } + } + + if(this.httprequest.desktop.kvm.connectionBar) + { + console.info1('Setting ConnectionBar request to NULL'); + this.httprequest.desktop.kvm.connectionBar.httprequest = null; + } + + this.httprequest = null; + this.desktop.tunnel = null; +} + +function kvm_tunnel_consentpromise_closehandler() +{ + if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); } +} + +function kvm_consentpromise_rejected(e) +{ + // User Consent Denied/Failed + this.ws._consentpromise = null; + MeshServerLogEx(34, null, "Failed to start remote desktop after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.ws = null; +} +function kvm_consentpromise_resolved(always) +{ + if (always && process.platform=='win32') { server_set_consentTimer(this.ws.httprequest.userid); } + + // Success + this.ws._consentpromise = null; + MeshServerLogEx(30, null, "Starting remote desktop after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null, msgid: 0 })); + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 1)) + { + // User Notifications is required + var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.ws.httprequest.realname); + var notifyTitle = "MeshCentral"; + if (this.ws.httprequest.soptions != null) + { + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } + if (this.ws.httprequest.soptions.notifyMsgDesktop != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgDesktop.replace('{0}', this.ws.httprequest.realname).replace('{1}', this.ws.httprequest.username); } + } + try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { } + } + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 0x40)) + { + // Connection Bar is required + if (this.ws.httprequest.desktop.kvm.connectionBar) + { + this.ws.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); + this.ws.httprequest.desktop.kvm.connectionBar.close(); + } + try + { + this.ws.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(this.ws.httprequest.privacybartext.replace('{0}', this.ws.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', this.ws.httprequest.desktop.kvm.users.join(', ')), require('MeshAgent')._tsid, color_options); + MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + } catch (ex) + { + if (process.platform != 'darwin') + { + MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or Not Supported (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + } + } + if (this.ws.httprequest.desktop.kvm.connectionBar) + { + this.ws.httprequest.desktop.kvm.connectionBar.state = + { + userid: this.ws.httprequest.userid, + xuserid: this.ws.httprequest.xuserid, + username: this.ws.httprequest.username, + sessionid: this.ws.httprequest.sessionid, + remoteaddr: this.ws.httprequest.remoteaddr, + guestname: this.ws.httprequest.guestname, + desktop: this.ws.httprequest.desktop + }; + this.ws.httprequest.desktop.kvm.connectionBar.on('close', function () + { + MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.state.remoteaddr + ")", state); + for (var i in this.state.desktop.kvm._pipedStreams) + { + this.state.desktop.kvm._pipedStreams[i].end(); + } + this.state.desktop.kvm.end(); + }); + } + } + this.ws.httprequest.desktop.kvm.pipe(this.ws, { dataTypeSkip: 1 }); + if (this.ws.httprequest.autolock) + { + destopLockHelper_pipe(this.ws.httprequest); + } + this.ws.resume(); + this.ws = null; +} + +function files_consentpromise_resolved(always) +{ + if (always && process.platform == 'win32') { server_set_consentTimer(this.ws.httprequest.userid); } + + // Success + this.ws._consentpromise = null; + MeshServerLogEx(40, null, "Starting remote files after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null })); + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 4)) + { + // User Notifications is required + var notifyMessage = currentTranslation['fileNotify'].replace('{0}', this.ws.httprequest.realname); + var notifyTitle = "MeshCentral"; + if (this.ws.httprequest.soptions != null) + { + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } + if (this.ws.httprequest.soptions.notifyMsgFiles != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgFiles.replace('{0}', this.ws.httprequest.realname).replace('{1}', this.ws.httprequest.username); } + } + try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { } + } + this.ws.resume(); + this.ws = null; +} +function files_consentpromise_rejected(e) +{ + // User Consent Denied/Failed + this.ws._consentpromise = null; + MeshServerLogEx(41, null, "Failed to start remote files after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.ws = null; +} +function files_tunnel_endhandler() +{ + if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); } +} + +function onTunnelData(data) +{ //sendConsoleText('OnTunnelData, ' + data.length + ', ' + typeof data + ', ' + data); // If this is upload data, save it to file @@ -2080,7 +2641,8 @@ function onTunnelData(data) { // // Check user access rights for terminal - if (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) == 0) || ((this.httprequest.rights != 0xFFFFFFFF) && ((this.httprequest.rights & MESHRIGHT_NOTERMINAL) != 0))) { + if (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) == 0) || ((this.httprequest.rights != 0xFFFFFFFF) && ((this.httprequest.rights & MESHRIGHT_NOTERMINAL) != 0))) + { // Disengage this tunnel, user does not have the rights to do this!! this.httprequest.protocol = 999999; this.httprequest.s.end(); @@ -2090,7 +2652,8 @@ function onTunnelData(data) { this.descriptorMetadata = "Remote Terminal"; - if (process.platform == 'win32') { + if (process.platform == 'win32') + { if (!require('win-terminal').PowerShellCapable() && (this.httprequest.protocol == 6 || this.httprequest.protocol == 9)) { this.httprequest.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: 'PowerShell is not supported on this version of windows', msgid: 1 })); this.httprequest.s.end(); @@ -2099,53 +2662,29 @@ function onTunnelData(data) { } var prom = require('promise'); - this.httprequest.tpromise = new prom(function (res, rej) { this._res = res; this._rej = rej; }); + this.httprequest.tpromise = new prom(promise_init); this.httprequest.tpromise.that = this; this.httprequest.tpromise.httprequest = this.httprequest; - - this.end = function () { - if (this.httprequest.tpromise._consent) { this.httprequest.tpromise._consent.close(); } - if (this.httprequest.connectionPromise) { this.httprequest.connectionPromise._rej('Closed'); } - - // Remove the terminal session to the count to update the server - if (this.httprequest.userid != null) { - var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest); - if (tunnelUserCount.terminal[userid] != null) { tunnelUserCount.terminal[userid]--; if (tunnelUserCount.terminal[userid] <= 0) { delete tunnelUserCount.terminal[userid]; } } - try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { } - broadcastSessionsToRegisteredApps(); - } - - if (process.platform == 'win32') { - // Unpipe the web socket - this.unpipe(this.httprequest._term); - if (this.httprequest._term) { this.httprequest._term.unpipe(this); } - - // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends). - if (this.rtcchannel) { - this.rtcchannel.unpipe(this.httprequest._term); - if (this.httprequest._term) { this.httprequest._term.unpipe(this.rtcchannel); } - } - - // Clean up - if (this.httprequest._term) { this.httprequest._term.end(); } - this.httprequest._term = null; - } - }; + this.end = terminal_end; // Perform User-Consent if needed. - if (this.httprequest.consent && (this.httprequest.consent & 16)) { + if (this.httprequest.consent && (this.httprequest.consent & 16)) + { this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); var consentMessage = currentTranslation['terminalConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); var consentTitle = 'MeshCentral'; - if (this.httprequest.soptions != null) { + if (this.httprequest.soptions != null) + { if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; } if (this.httprequest.soptions.consentMsgTerminal != null) { consentMessage = this.httprequest.soptions.consentMsgTerminal.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } } - if (process.platform == 'win32') { + if (process.platform == 'win32') + { var enhanced = false; try { require('win-userconsent'); enhanced = true; } catch (ex) { } - if (enhanced) { + if (enhanced) + { var ipr = server_getUserImage(this.httprequest.userid); ipr.consentTitle = consentTitle; ipr.consentMessage = consentMessage; @@ -2153,21 +2692,25 @@ function onTunnelData(data) { ipr.consentAutoAccept = this.httprequest.consentAutoAccept; ipr.username = this.httprequest.realname; ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage }; - this.httprequest.tpromise._consent = ipr.then(function (img) { + this.httprequest.tpromise._consent = ipr.then(function (img) + { this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground }); this.__childPromise.close = this.consent.close.bind(this.consent); return (this.consent); }); - } else { - this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.consentTimeout); + } else + { + this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout); } - } else { - this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.consentTimeout); + } else + { + this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout); } this.httprequest.tpromise._consent.retPromise = this.httprequest.tpromise; this.httprequest.tpromise._consent.then( - function (always) { - if (always) { server_set_consentTimer(this.retPromise.httprequest.userid); } + function (always) + { + if (always && process.platform == 'win32') { server_set_consentTimer(this.retPromise.httprequest.userid); } // Success MeshServerLogEx(27, null, "Local user accepted remote terminal request (" + this.retPromise.httprequest.remoteaddr + ")", this.retPromise.that.httprequest); @@ -2179,174 +2722,21 @@ function onTunnelData(data) { // Denied MeshServerLogEx(28, null, "Local user rejected remote terminal request (" + this.retPromise.that.httprequest.remoteaddr + ")", this.retPromise.that.httprequest); this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.retPromise._consent = null; this.retPromise._rej(e.toString()); }); } - else { + else + { // User-Consent is not required, so just resolve this promise this.httprequest.tpromise._res(); } - this.httprequest.tpromise.then( - function () { - this.httprequest.connectionPromise = new prom(function (res, rej) { this._res = res; this._rej = rej; }); - this.httprequest.connectionPromise.ws = this.that; - - // Start Terminal - if (process.platform == 'win32') { - try { - var cols = 80, rows = 25; - if (this.httprequest.xoptions) { - if (this.httprequest.xoptions.rows) { rows = this.httprequest.xoptions.rows; } - if (this.httprequest.xoptions.cols) { cols = this.httprequest.xoptions.cols; } - } - - if ((this.httprequest.protocol == 1) || (this.httprequest.protocol == 6)) { - // Admin Terminal - if (require('win-virtual-terminal').supported) { - // ConPTY PseudoTerminal - // this.httprequest._term = require('win-virtual-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](80, 25); - - // The above line is commented out, because there is a bug with ClosePseudoConsole() API, so this is the workaround - this.httprequest._dispatcher = require('win-dispatcher').dispatch({ modules: [{ name: 'win-virtual-terminal', script: getJSModule('win-virtual-terminal') }], launch: { module: 'win-virtual-terminal', method: (this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'), args: [cols, rows] } }); - this.httprequest._dispatcher.httprequest = this.httprequest; - this.httprequest._dispatcher.on('connection', function (c) { if (this.httprequest.connectionPromise.completed) { c.end(); } else { this.httprequest.connectionPromise._res(c); } }); - } - else { - // Legacy Terminal - this.httprequest.connectionPromise._res(require('win-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](cols, rows)); - } - } - else { - // Logged in user - var userPromise = require('user-sessions').enumerateUsers(); - userPromise.that = this; - userPromise.then(function (u) { - var that = this.that; - if (u.Active.length > 0) { - var username = '"' + u.Active[0].Domain + '\\' + u.Active[0].Username + '"'; - //sendConsoleText('Terminal: ' + username); - if (require('win-virtual-terminal').supported) { - // ConPTY PseudoTerminal - that.httprequest._dispatcher = require('win-dispatcher').dispatch({ user: username, modules: [{ name: 'win-virtual-terminal', script: getJSModule('win-virtual-terminal') }], launch: { module: 'win-virtual-terminal', method: (that.httprequest.protocol == 9 ? 'StartPowerShell' : 'Start'), args: [cols, rows] } }); - } - else { - // Legacy Terminal - that.httprequest._dispatcher = require('win-dispatcher').dispatch({ user: username, modules: [{ name: 'win-terminal', script: getJSModule('win-terminal') }], launch: { module: 'win-terminal', method: (that.httprequest.protocol == 9 ? 'StartPowerShell' : 'Start'), args: [cols, rows] } }); - } - that.httprequest._dispatcher.ws = that; - that.httprequest._dispatcher.on('connection', function (c) { if (this.ws.httprequest.connectionPromise.completed) { c.end(); } else { this.ws.httprequest.connectionPromise._res(c); } }); - } - }); - } - } catch (ex) { - this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString()); - } - } - else { - try { - var bash = fs.existsSync('/bin/bash') ? '/bin/bash' : false; - var sh = fs.existsSync('/bin/sh') ? '/bin/sh' : false; - var login = process.platform == 'linux' ? '/bin/login' : '/usr/bin/login'; - - var env = { HISTCONTROL: 'ignoreboth' }; - if (process.env['LANG']) { env['LANG'] = process.env['LANG']; } - if (process.env['PATH']) { env['PATH'] = process.env['PATH']; } - if (this.httprequest.xoptions) { - if (this.httprequest.xoptions.rows) { env.LINES = ('' + this.httprequest.xoptions.rows); } - if (this.httprequest.xoptions.cols) { env.COLUMNS = ('' + this.httprequest.xoptions.cols); } - } - var options = { type: childProcess.SpawnTypes.TERM, uid: (this.httprequest.protocol == 8) ? require('user-sessions').consoleUid() : null, env: env }; - if (this.httprequest.xoptions && this.httprequest.xoptions.requireLogin) { - if (!require('fs').existsSync(login)) { throw ('Unable to spawn login process'); } - this.httprequest.connectionPromise._res(childProcess.execFile(login, ['login'], options)); // Start login shell - } - else if (bash) { - var p = childProcess.execFile(bash, ['bash'], options); // Start bash - // Spaces at the beginning of lines are needed to hide commands from the command history - if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string')) { - if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); } - } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); } - this.httprequest.connectionPromise._res(p); - } - else if (sh) { - var p = childProcess.execFile(sh, ['sh'], options); // Start sh - // Spaces at the beginning of lines are needed to hide commands from the command history - if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string')) { - if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); } - } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); } - this.httprequest.connectionPromise._res(p); - } - else { - this.httprequest.connectionPromise._rej('Failed to start remote terminal session, no shell found'); - } - } catch (ex) { - this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString()); - } - } - - this.httprequest.connectionPromise.then( - function (term) { - // SUCCESS - var stdoutstream; - var stdinstream; - if (process.platform == 'win32') { - this.ws.httprequest._term = term; - this.ws.httprequest._term.tunnel = this.ws; - stdoutstream = stdinstream = term; - } - else { - term.descriptorMetadata = 'Remote Terminal'; - this.ws.httprequest.process = term; - this.ws.httprequest.process.tunnel = this.ws; - term.stderr.stdout = term.stdout; - term.stderr.on('data', function (c) { this.stdout.write(c); }); - stdoutstream = term.stdout; - stdinstream = term.stdin; - this.ws.prependListener('end', function () { this.httprequest.process.kill(); }); - term.prependListener('exit', function () { this.tunnel.end(); }); - } - - this.ws.removeAllListeners('data'); - this.ws.on('data', onTunnelControlData); - - stdoutstream.pipe(this.ws, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text. - this.ws.pipe(stdinstream, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text. - - // Add the terminal session to the count to update the server - if (this.ws.httprequest.userid != null) { - var userid = getUserIdAndGuestNameFromHttpRequest(this.ws.httprequest); - if (tunnelUserCount.terminal[userid] == null) { tunnelUserCount.terminal[userid] = 1; } else { tunnelUserCount.terminal[userid]++; } - try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { } - broadcastSessionsToRegisteredApps(); - } - - // Toast Notification, if required - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 2)) { - // User Notifications is required - var notifyMessage = currentTranslation['terminalNotify'].replace('{0}', this.ws.httprequest.username); - var notifyTitle = "MeshCentral"; - if (this.ws.httprequest.soptions != null) { - if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } - if (this.ws.httprequest.soptions.notifyMsgTerminal != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgTerminal.replace('{0}', this.ws.httprequest.realname).replace('{1}', this.ws.httprequest.username); } - } - try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { } - } - }, - function (e) { - // FAILED to connect terminal - this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - this.ws.end(); - }); - }, - function (e) { - // DO NOT start terminal - this.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - this.that.end(); - }); + this.httprequest.tpromise.then(terminal_promise_consent_resolved, terminal_promise_consent_rejected); } - else if (this.httprequest.protocol == 2) { + else if (this.httprequest.protocol == 2) + { // // Remote Desktop // @@ -2378,14 +2768,17 @@ function onTunnelData(data) { // Send a metadata update to all desktop sessions var users = {}; - if (this.httprequest.desktop.kvm.tunnels != null) { - for (var i in this.httprequest.desktop.kvm.tunnels) { + if (this.httprequest.desktop.kvm.tunnels != null) + { + for (var i in this.httprequest.desktop.kvm.tunnels) + { try { var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest.desktop.kvm.tunnels[i].httprequest); if (users[userid] == null) { users[userid] = 1; } else { users[userid]++; } } catch (ex) { sendConsoleText(ex); } } - for (var i in this.httprequest.desktop.kvm.tunnels) { + for (var i in this.httprequest.desktop.kvm.tunnels) + { try { this.httprequest.desktop.kvm.tunnels[i].write(JSON.stringify({ ctrlChannel: '102938', type: 'metadata', users: users })); } catch (ex) { } } tunnelUserCount.desktop = users; @@ -2393,79 +2786,8 @@ function onTunnelData(data) { broadcastSessionsToRegisteredApps(); } - this.end = function () { - --this.desktop.kvm.connectionCount; + this.end = tunnel_kvm_end; - // Remove ourself from the list of remote desktop session - var i = this.desktop.kvm.tunnels.indexOf(this); - if (i >= 0) { this.desktop.kvm.tunnels.splice(i, 1); } - - // Send a metadata update to all desktop sessions - var users = {}; - if (this.httprequest.desktop.kvm.tunnels != null) { - for (var i in this.httprequest.desktop.kvm.tunnels) { - try { - var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest.desktop.kvm.tunnels[i].httprequest); - if (users[userid] == null) { users[userid] = 1; } else { users[userid]++; } - } catch (ex) { sendConsoleText(ex); } - } - for (var i in this.httprequest.desktop.kvm.tunnels) { - try { this.httprequest.desktop.kvm.tunnels[i].write(JSON.stringify({ ctrlChannel: '102938', type: 'metadata', users: users })); } catch (ex) { } - } - tunnelUserCount.desktop = users; - try { mesh.SendCommand({ action: 'sessions', type: 'kvm', value: users }); } catch (ex) { } - broadcastSessionsToRegisteredApps(); - } - - // Unpipe the web socket - try { - this.unpipe(this.httprequest.desktop.kvm); - this.httprequest.desktop.kvm.unpipe(this); - } catch (ex) { } - - // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends). - if (this.rtcchannel) { - try { - this.rtcchannel.unpipe(this.httprequest.desktop.kvm); - this.httprequest.desktop.kvm.unpipe(this.rtcchannel); - } - catch (ex) { } - } - - // Place wallpaper back if needed - // TODO - - if (this.desktop.kvm.connectionCount == 0) { - // Display a toast message. This may not be supported on all platforms. - // try { require('toaster').Toast('MeshCentral', 'Remote Desktop Control Ended.'); } catch (ex) { } - - this.httprequest.desktop.kvm.end(); - if (this.httprequest.desktop.kvm.connectionBar) { - this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); - this.httprequest.desktop.kvm.connectionBar.close(); - this.httprequest.desktop.kvm.connectionBar = null; - } - } else { - for (var i in this.httprequest.desktop.kvm.users) { - if ((this.httprequest.desktop.kvm.users[i] == this.httprequest.username) && this.httprequest.desktop.kvm.connectionBar) { - for (var j in this.httprequest.desktop.kvm.rusers) { if (this.httprequest.desktop.kvm.rusers[j] == this.httprequest.realname) { this.httprequest.desktop.kvm.rusers.splice(j, 1); break; } } - this.httprequest.desktop.kvm.users.splice(i, 1); - this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); - this.httprequest.desktop.kvm.connectionBar.close(); - this.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(this.httprequest.privacybartext.replace('{0}', this.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', this.httprequest.desktop.kvm.users.join(', ')), require('MeshAgent')._tsid, color_options); - this.httprequest.desktop.kvm.connectionBar.httprequest = this.httprequest; - this.httprequest.desktop.kvm.connectionBar.on('close', function () { - MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest); - for (var i in this.httprequest.desktop.kvm._pipedStreams) { - this.httprequest.desktop.kvm._pipedStreams[i].end(); - } - this.httprequest.desktop.kvm.end(); - }); - break; - } - } - } - }; if (this.httprequest.desktop.kvm.hasOwnProperty('connectionCount')) { this.httprequest.desktop.kvm.connectionCount++; this.httprequest.desktop.kvm.rusers.push(this.httprequest.realname); @@ -2478,31 +2800,38 @@ function onTunnelData(data) { this.httprequest.desktop.kvm.users = [this.httprequest.username]; } - if ((this.httprequest.desktopviewonly != true) && ((this.httprequest.rights == 0xFFFFFFFF) || (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) != 0) && ((this.httprequest.rights & MESHRIGHT_REMOTEVIEW) == 0)))) { + if ((this.httprequest.desktopviewonly != true) && ((this.httprequest.rights == 0xFFFFFFFF) || (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) != 0) && ((this.httprequest.rights & MESHRIGHT_REMOTEVIEW) == 0)))) + { // If we have remote control rights, pipe the KVM input this.pipe(this.httprequest.desktop.kvm, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text. Pipe the Browser --> KVM input. - } else { + } + else + { // We need to only pipe non-mouse & non-keyboard inputs. // sendConsoleText('Warning: No Remote Desktop Input Rights.'); // TODO!!! } // Perform notification if needed. Toast messages may not be supported on all platforms. - if (this.httprequest.consent && (this.httprequest.consent & 8)) { + if (this.httprequest.consent && (this.httprequest.consent & 8)) + { // User Consent Prompt is required // Send a console message back using the console channel, "\n" is supported. this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); var consentMessage = currentTranslation['desktopConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); var consentTitle = 'MeshCentral'; - if (this.httprequest.soptions != null) { + if (this.httprequest.soptions != null) + { if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; } if (this.httprequest.soptions.consentMsgDesktop != null) { consentMessage = this.httprequest.soptions.consentMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } } var pr; - if (process.platform == 'win32') { + if (process.platform == 'win32') + { var enhanced = false; try { require('win-userconsent'); enhanced = true; } catch (ex) { } - if (enhanced) { + if (enhanced) + { var ipr = server_getUserImage(this.httprequest.userid); ipr.consentTitle = consentTitle; ipr.consentMessage = consentMessage; @@ -2511,85 +2840,33 @@ function onTunnelData(data) { ipr.tsid = tsid; ipr.username = this.httprequest.realname; ipr.translation = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage }; - pr = ipr.then(function (img) { + pr = ipr.then(function (img) + { this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), uid: this.tsid, timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translation, background: color_options.background, foreground: color_options.foreground }); this.__childPromise.close = this.consent.close.bind(this.consent); return (this.consent); }); } - else { - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null, tsid); + else + { + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null, tsid); } } - else { - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null, tsid); + else + { + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null, tsid); } pr.ws = this; this.pause(); this._consentpromise = pr; - this.prependOnceListener('end', function () { - if (this._consentpromise && this._consentpromise.close) { - this._consentpromise.close(); - } - }); - pr.then( - function (always) { - if (always) { server_set_consentTimer(this.ws.httprequest.userid); } - - // Success - this.ws._consentpromise = null; - MeshServerLogEx(30, null, "Starting remote desktop after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null, msgid: 0 })); - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 1)) { - // User Notifications is required - var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.ws.httprequest.realname); - var notifyTitle = "MeshCentral"; - if (this.ws.httprequest.soptions != null) { - if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } - if (this.ws.httprequest.soptions.notifyMsgDesktop != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgDesktop.replace('{0}', this.ws.httprequest.realname).replace('{1}', this.ws.httprequest.username); } - } - try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { } - } - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 0x40)) { - // Connection Bar is required - if (this.ws.httprequest.desktop.kvm.connectionBar) { - this.ws.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); - this.ws.httprequest.desktop.kvm.connectionBar.close(); - } - try { - this.ws.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(this.ws.httprequest.privacybartext.replace('{0}', this.ws.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', this.ws.httprequest.desktop.kvm.users.join(', ')), require('MeshAgent')._tsid, color_options); - MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - } catch (ex) { - if (process.platform != 'darwin') { - MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or Not Supported (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - } - } - if (this.ws.httprequest.desktop.kvm.connectionBar) { - this.ws.httprequest.desktop.kvm.connectionBar.httprequest = this.ws.httprequest; - this.ws.httprequest.desktop.kvm.connectionBar.on('close', function () { - MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest); - for (var i in this.httprequest.desktop.kvm._pipedStreams) { - this.httprequest.desktop.kvm._pipedStreams[i].end(); - } - this.httprequest.desktop.kvm.end(); - }); - } - } - this.ws.httprequest.desktop.kvm.pipe(this.ws, { dataTypeSkip: 1 }); - if (this.ws.httprequest.autolock) { - destopLockHelper_pipe(this.ws.httprequest); - } - this.ws.resume(); - }, - function (e) { - // User Consent Denied/Failed - this.ws._consentpromise = null; - MeshServerLogEx(34, null, "Failed to start remote desktop after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - }); - } else { + this.prependOnceListener('end', kvm_tunnel_consentpromise_closehandler); + pr.then(kvm_consentpromise_resolved, kvm_consentpromise_rejected); + } + else + { // User Consent Prompt is not required - if (this.httprequest.consent && (this.httprequest.consent & 1)) { + if (this.httprequest.consent && (this.httprequest.consent & 1)) + { // User Notifications is required MeshServerLogEx(35, null, "Started remote desktop with toast notification (" + this.httprequest.remoteaddr + ")", this.httprequest); var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.httprequest.realname); @@ -2599,34 +2876,52 @@ function onTunnelData(data) { if (this.httprequest.soptions.notifyMsgDesktop != null) { notifyMessage = this.httprequest.soptions.notifyMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } } try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { } - } else { + } else + { MeshServerLogEx(36, null, "Started remote desktop without notification (" + this.httprequest.remoteaddr + ")", this.httprequest); } - if (this.httprequest.consent && (this.httprequest.consent & 0x40)) { + if (this.httprequest.consent && (this.httprequest.consent & 0x40)) + { // Connection Bar is required - if (this.httprequest.desktop.kvm.connectionBar) { + if (this.httprequest.desktop.kvm.connectionBar) + { this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close'); this.httprequest.desktop.kvm.connectionBar.close(); } - try { + try + { this.httprequest.desktop.kvm.connectionBar = require('notifybar-desktop')(this.httprequest.privacybartext.replace('{0}', this.httprequest.desktop.kvm.rusers.join(', ')).replace('{1}', this.httprequest.desktop.kvm.users.join(', ')), require('MeshAgent')._tsid, color_options); MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.httprequest.remoteaddr + ")", this.httprequest); } catch (ex) { MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or not Supported (" + this.httprequest.remoteaddr + ")", this.httprequest); } - if (this.httprequest.desktop.kvm.connectionBar) { - this.httprequest.desktop.kvm.connectionBar.httprequest = this.httprequest; - this.httprequest.desktop.kvm.connectionBar.on('close', function () { - MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest); - for (var i in this.httprequest.desktop.kvm._pipedStreams) { - this.httprequest.desktop.kvm._pipedStreams[i].end(); + if (this.httprequest.desktop.kvm.connectionBar) + { + this.httprequest.desktop.kvm.connectionBar.state = + { + userid: this.httprequest.userid, + xuserid: this.httprequest.xuserid, + username: this.httprequest.username, + sessionid: this.httprequest.sessionid, + remoteaddr: this.httprequest.remoteaddr, + guestname: this.httprequest.guestname, + desktop: this.httprequest.desktop + }; + this.httprequest.desktop.kvm.connectionBar.on('close', function () + { + console.info1('Connection Bar Forcefully closed'); + MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.state.remoteaddr + ")", this.state); + for (var i in this.state.desktop.kvm._pipedStreams) + { + this.state.desktop.kvm._pipedStreams[i].end(); } - this.httprequest.desktop.kvm.end(); + this.state.desktop.kvm.end(); }); } } this.httprequest.desktop.kvm.pipe(this, { dataTypeSkip: 1 }); - if (this.httprequest.autolock) { + if (this.httprequest.autolock) + { destopLockHelper_pipe(this.httprequest); } } @@ -2634,7 +2929,6 @@ function onTunnelData(data) { this.removeAllListeners('data'); this.on('data', onTunnelControlData); //this.write('MeshCore KVM Hello!1'); - } else if (this.httprequest.protocol == 5) { // // Remote Files @@ -2659,7 +2953,8 @@ function onTunnelData(data) { broadcastSessionsToRegisteredApps(); } - this.end = function () { + this.end = function () + { // Remove the files session from the count to update the server if (this.httprequest.userid != null) { var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest); @@ -2670,22 +2965,26 @@ function onTunnelData(data) { }; // Perform notification if needed. Toast messages may not be supported on all platforms. - if (this.httprequest.consent && (this.httprequest.consent & 32)) { + if (this.httprequest.consent && (this.httprequest.consent & 32)) + { // User Consent Prompt is required // Send a console message back using the console channel, "\n" is supported. this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); var consentMessage = currentTranslation['fileConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); var consentTitle = 'MeshCentral'; - if (this.httprequest.soptions != null) { + if (this.httprequest.soptions != null) + { if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; } if (this.httprequest.soptions.consentMsgFiles != null) { consentMessage = this.httprequest.soptions.consentMsgFiles.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); } } var pr; - if (process.platform == 'win32') { + if (process.platform == 'win32') + { var enhanced = false; try { require('win-userconsent'); enhanced = true; } catch (ex) { } - if (enhanced) { + if (enhanced) + { var ipr = server_getUserImage(this.httprequest.userid); ipr.consentTitle = consentTitle; ipr.consentMessage = consentMessage; @@ -2693,49 +2992,29 @@ function onTunnelData(data) { ipr.consentAutoAccept = this.httprequest.consentAutoAccept; ipr.username = this.httprequest.realname; ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage }; - pr = ipr.then(function (img) { + pr = ipr.then(function (img) + { this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground }); this.__childPromise.close = this.consent.close.bind(this.consent); return (this.consent); }); - } else { - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null); + } else + { + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null); } - } else { - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null); + } + else + { + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null); } pr.ws = this; this.pause(); this._consentpromise = pr; - this.prependOnceListener('end', function () { if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); } }); - pr.then( - function (always) { - if (always) { server_set_consentTimer(this.ws.httprequest.userid); } - - // Success - this.ws._consentpromise = null; - MeshServerLogEx(40, null, "Starting remote files after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null })); - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 4)) { - // User Notifications is required - var notifyMessage = currentTranslation['fileNotify'].replace('{0}', this.ws.httprequest.realname); - var notifyTitle = "MeshCentral"; - if (this.ws.httprequest.soptions != null) { - if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } - if (this.ws.httprequest.soptions.notifyMsgFiles != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgFiles.replace('{0}', this.ws.httprequest.realname).replace('{1}', this.ws.httprequest.username); } - } - try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { } - } - this.ws.resume(); - }, - function (e) { - // User Consent Denied/Failed - this.ws._consentpromise = null; - MeshServerLogEx(41, null, "Failed to start remote files after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); - this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); - }); + this.prependOnceListener('end', files_tunnel_endhandler); + pr.then(files_consentpromise_resolved, files_consentpromise_rejected); } - else { + else + { // User Consent Prompt is not required if (this.httprequest.consent && (this.httprequest.consent & 4)) { // User Notifications is required @@ -3062,6 +3341,44 @@ function onTunnelWebRTCControlData(data) { } } +function tunnel_webrtc_onEnd() +{ + // The WebRTC channel closed, unpipe the KVM now. This is also done when the web socket closes. + //sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC data channel closed'); + if (this.websocket.desktop && this.websocket.desktop.kvm) + { + try + { + this.unpipe(this.websocket.desktop.kvm); + this.websocket.httprequest.desktop.kvm.unpipe(this); + } catch (ex) { } + } + this.httprequest = null; + this.websocket = null; +} +function tunnel_webrtc_DataChannel_OnFinalized() +{ + console.info1('WebRTC DataChannel Finalized'); +} +function tunnel_webrtc_OnDataChannel(rtcchannel) +{ + //sendConsoleText('WebRTC Datachannel open, protocol: ' + this.websocket.httprequest.protocol); + //rtcchannel.maxFragmentSize = 32768; + rtcchannel.xrtc = this; + rtcchannel.websocket = this.websocket; + this.rtcchannel = rtcchannel; + this.rtcchannel.once('~', tunnel_webrtc_DataChannel_OnFinalized); + this.websocket.rtcchannel = rtcchannel; + this.websocket.rtcchannel.on('data', onTunnelWebRTCControlData); + this.websocket.rtcchannel.on('end', tunnel_webrtc_onEnd); + this.websocket.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc0\"}'); // Indicate we are ready for WebRTC switch-over. +} + +function tunnel_webrtc_OnFinalized() +{ + console.info1('WebRTC Connection Finalized'); +} + // Called when receiving control data on websocket function onTunnelControlData(data, ws) { var obj; @@ -3135,7 +3452,8 @@ function onTunnelControlData(data, ws) { break; } case 'webrtc0': { // Browser indicates we can start WebRTC switch-over. - if (ws.httprequest.protocol == 1) { // Terminal + if (ws.httprequest.protocol == 1) + { // Terminal // This is a terminal data stream, unpipe the terminal now and indicate to the other side that terminal data will no longer be received over WebSocket if (process.platform == 'win32') { ws.httprequest._term.unpipe(ws); @@ -3146,7 +3464,8 @@ function onTunnelControlData(data, ws) { } else if (ws.httprequest.protocol == 2) { // Desktop // This is a KVM data stream, unpipe the KVM now and indicate to the other side that KVM data will no longer be received over WebSocket ws.httprequest.desktop.kvm.unpipe(ws); - } else { + } else + { // Switch things around so all WebRTC data goes to onTunnelData(). ws.rtcchannel.httprequest = ws.httprequest; ws.rtcchannel.removeAllListeners('data'); @@ -3155,8 +3474,10 @@ function onTunnelControlData(data, ws) { ws.write("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc1\"}"); // End of data marker break; } - case 'webrtc1': { - if ((ws.httprequest.protocol == 1) || (ws.httprequest.protocol == 6)) { // Terminal + case 'webrtc1': + { + if ((ws.httprequest.protocol == 1) || (ws.httprequest.protocol == 6)) + { // Terminal // Switch the user input from websocket to webrtc at this point. if (process.platform == 'win32') { ws.unpipe(ws.httprequest._term); @@ -3166,7 +3487,9 @@ function onTunnelControlData(data, ws) { ws.rtcchannel.pipe(ws.httprequest.process.stdin, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text. } ws.resume(); // Resume the websocket to keep receiving control data - } else if (ws.httprequest.protocol == 2) { // Desktop + } + else if (ws.httprequest.protocol == 2) + { // Desktop // Switch the user input from websocket to webrtc at this point. ws.unpipe(ws.httprequest.desktop.kvm); try { ws.webrtc.rtcchannel.pipe(ws.httprequest.desktop.kvm, { dataTypeSkip: 1, end: false }); } catch (ex) { sendConsoleText('EX2'); } // 0 = Binary, 1 = Text. @@ -3193,29 +3516,12 @@ function onTunnelControlData(data, ws) { // This is a WebRTC offer. if ((ws.httprequest.protocol == 1) || (ws.httprequest.protocol == 6)) return; // TODO: Terminal is currently broken with WebRTC. Reject WebRTC upgrade for now. ws.webrtc = rtc.createConnection(); + ws.webrtc.once('~', tunnel_webrtc_OnFinalized); ws.webrtc.websocket = ws; - ws.webrtc.on('connected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC connected');*/ }); - ws.webrtc.on('disconnected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC disconnected');*/ }); - ws.webrtc.on('dataChannel', function (rtcchannel) { - //sendConsoleText('WebRTC Datachannel open, protocol: ' + this.websocket.httprequest.protocol); - //rtcchannel.maxFragmentSize = 32768; - rtcchannel.xrtc = this; - rtcchannel.websocket = this.websocket; - this.rtcchannel = rtcchannel; - this.websocket.rtcchannel = rtcchannel; - this.websocket.rtcchannel.on('data', onTunnelWebRTCControlData); - this.websocket.rtcchannel.on('end', function () { - // The WebRTC channel closed, unpipe the KVM now. This is also done when the web socket closes. - //sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC data channel closed'); - if (this.websocket.desktop && this.websocket.desktop.kvm) { - try { - this.unpipe(this.websocket.desktop.kvm); - this.websocket.httprequest.desktop.kvm.unpipe(this); - } catch (ex) { } - } - }); - this.websocket.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc0\"}'); // Indicate we are ready for WebRTC switch-over. - }); + //ws.webrtc.on('connected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC connected');*/ }); + //ws.webrtc.on('disconnected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC disconnected');*/ }); + ws.webrtc.on('dataChannel', tunnel_webrtc_OnDataChannel); + var sdp = null; try { sdp = ws.webrtc.setOffer(obj.sdp); } catch (ex) { } if (sdp != null) { ws.write({ type: 'answer', ctrlChannel: '102938', sdp: sdp }); } @@ -4040,7 +4346,9 @@ function processConsoleCommand(cmd, args, rights, sessionid) { this._dispatcher.on('connection', function (c) { this._c = c; this._c.root = this.parent; - this._c.on('end', function () { + this._c.on('end', function () + { + this.root._dispatcher.close(); this.root._dispatcher = null; this.root = null; }); diff --git a/apprelays.js b/apprelays.js index 569c63b0..3eb87a03 100644 --- a/apprelays.js +++ b/apprelays.js @@ -69,7 +69,7 @@ function SerialTunnel(options) { } // Construct a Web relay object -module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid, sessionid) { +module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid, sessionid, expire) { const obj = {}; obj.parent = parent; obj.lastOperation = Date.now(); @@ -80,6 +80,7 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, obj.port = port; obj.appid = appid; obj.sessionid = sessionid; + obj.expireTimer = null; var pendingRequests = []; var nextTunnelId = 1; var tunnels = {}; @@ -90,6 +91,9 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, // Any HTTP cookie set by the device is going to be shared between all tunnels to that device. obj.webCookies = {}; + // Setup an expire time if needed + if (expire != null) { var timeout = (expire - Date.now()); if (timeout < 10) { timeout = 10; } obj.expireTimer = setTimeout(close, timeout); } + // Events obj.closed = false; obj.onclose = null; @@ -202,6 +206,9 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, parent.parent.debug('webrelay', 'tunnel-close'); obj.closed = true; + // Clear the time if present + if (obj.expireTimer != null) { clearTimeout(obj.expireTimer); delete obj.expireTimer; } + // Close all tunnels for (var i in tunnels) { tunnels[i].close(); } tunnels = null; diff --git a/meshcentral.js b/meshcentral.js index 0dd31e92..44b6ee71 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -466,7 +466,7 @@ function CreateMeshCentralServer(config, args) { const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : ''); const env = Object.assign({}, process.env); // Shallow clone if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; } - const xxprocess = child_process.exec(npmpath + ' install meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { + const xxprocess = child_process.exec(npmpath + ' install --no-package-lock meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { if ((error != null) && (error != '')) { console.log('Update failed: ' + error); } }); xxprocess.data = ''; diff --git a/meshuser.js b/meshuser.js index 463faf0c..8972acf3 100644 --- a/meshuser.js +++ b/meshuser.js @@ -4137,8 +4137,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use else if ((command.start != null) && (typeof command.start != 'number')) { err = 'Invalid start time'; } // Check the start time in UTC seconds else if ((command.end != null) && (typeof command.end != 'number')) { err = 'Invalid end time'; } // Check the end time in UTC seconds else if (common.validateInt(command.consent, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags - else if (common.validateInt(command.p, 1, 7) == false) { err = 'Invalid protocol'; } // Check the protocol, 1 = Terminal, 2 = Desktop, 4 = Files + else if (common.validateInt(command.p, 1, 31) == false) { err = 'Invalid protocol'; } // Check the protocol, 1 = Terminal, 2 = Desktop, 4 = Files, 8 = HTTP, 16 = HTTPS else if ((command.recurring != null) && (common.validateInt(command.recurring, 1, 2) == false)) { err = 'Invalid recurring value'; } // Check the recurring value, 1 = Daily, 2 = Weekly + else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present else if ((command.recurring != null) && ((command.end != null) || (command.start == null) || (command.expire == null))) { err = 'Invalid recurring command'; } else if ((command.expire == null) && ((command.start == null) || (command.end == null) || (command.start > command.end))) { err = 'No time specified'; } // Check that a time range is present else { @@ -4238,7 +4239,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use try { ws.send(JSON.stringify(command)); } catch (ex) { } // Create a device sharing database entry - var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', xmeshid: node.meshid, nodeid: node._id, p: command.p, domain: node.domain, publicid: publicid, userid: user._id, guestName: command.guestname, consent: command.consent, url: url }; + var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', xmeshid: node.meshid, nodeid: node._id, p: command.p, domain: node.domain, publicid: publicid, userid: user._id, guestName: command.guestname, consent: command.consent, port: command.port, url: url }; if ((startTime != null) && (expireTime != null)) { shareEntry.startTime = startTime; shareEntry.expireTime = expireTime; } else if ((startTime != null) && (duration != null)) { shareEntry.startTime = startTime; shareEntry.duration = duration; } if (command.recurring) { shareEntry.recurring = command.recurring; } diff --git a/views/default.handlebars b/views/default.handlebars index ecdbce87..345ff633 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -3650,7 +3650,7 @@ if (message.consent & 0x0040) { y.push("Privacy bar"); } if (y.length == 0) { y.push("None"); } x += addHtmlValue("User Consent", y.join(', ')); - var type = ['', "Remote Terminal Link", "Remote Desktop Link", "Remote Desktop + Terminal Link", "Remote Files Link", "Remote Terminal + Files Link", "Remote Desktop + Files Link", "Remote Desktop + Terminal + Files Link"][message.p]; + var type = ''; if (message.p <= 7) { type = ['', "Remote Terminal Link", "Remote Desktop Link", "Remote Desktop + Terminal Link", "Remote Files Link", "Remote Terminal + Files Link", "Remote Desktop + Files Link", "Remote Desktop + Terminal + Files Link"][message.p]; } else if (message.p == 8) { type = format("HTTP/{0} link", message.port); } else if (message.p == 16) { type = format("HTTPS/{0}", message.port); } x += '
' + type + '
'; setDialogMode(2, "Share Device", 1, null, x); break; @@ -7588,7 +7588,7 @@ var dshare = deviceShares[i], trash = ''; if (dshare.url != null) { trash += ' '; } trash += ''; - var type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Files", "Terminal + Files", "Desktop + Files", "Desktop + Terminal + Files"][dshare.p]; + var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Files", "Terminal + Files", "Desktop + Files", "Desktop + Terminal + Files"][dshare.p]; } else if (dshare.p == 8) { type = "HTTP/" + dshare.port; } else if (dshare.p == 16) { type = "HTTPS/" + dshare.port; } var details = type; if ((dshare.startTime != null) && (dshare.expireTime != null)) { details = format("{0}, {1} to {2}", type, printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); } if ((dshare.startTime != null) && (dshare.duration != null)) { @@ -7896,6 +7896,11 @@ if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; } var allFeatures = ''; if ((rights != 0xFFFFFFFF) && ((rights & 0x700) != 0)) { allFeatures = ''; } + var httpFeature = ''; + if (webRelayPort != 0) { + httpFeature = ''; + if ((rights != 0xFFFFFFFF) && ((rights & 8) != 0)) { httpFeature = ''; } + } var y = '', z = ''; if ((currentNode.agent.caps & 1) == 1) { y += (deskFull + ''); } // Agent is desktop capable @@ -7904,6 +7909,7 @@ if ((currentNode.agent.caps & 5) == 5) { y += deskFiles; } // Agent is desktop + files capable if ((currentNode.agent.caps & 6) == 6) { y += termFiles; } // Agent is terminal + files capable if ((currentNode.agent.caps & 7) == 7) { y += allFeatures; } // Agent is desktop + terminal + files capable + y += httpFeature; x += addHtmlValue("Type", ''); var options = { 1 : "1 minute", 5 : "5 minutes", 10 : "10 minutes", 15 : "15 minutes", 30 : "30 minutes", 45 : "45 minutes", 60 : "60 minutes", 120 : "2 hours", 240 : "4 hours", 480 : "8 hours", 720 : "12 hours", 960 : "16 hours", 1440 : "24 hours", 2880 : "2 days", 5760 : "4 days", 0 : "Unlimited" } @@ -7925,7 +7931,9 @@ x += addHtmlValue("Start Time", ''); x += addHtmlValue("Duration", ''); x += ''; - if (currentNode.agent.caps & 1) { x += addHtmlValue("User Consent", ''); } + if (currentNode.agent.caps & 1) { x += '
' + addHtmlValue("User Consent", '') + '
'; } + x += '
' + addHtmlValue("Port", '') + '
'; + x += '
' + addHtmlValue("Port", '') + '
'; setDialogMode(2, "Share Device", 3, showShareDeviceEx, x); showShareDeviceValidate(); var tomorrow = new Date(); @@ -7936,18 +7944,25 @@ } function showShareDeviceValidate() { + if (currentNode.agent.caps & 1) { QV('d2userConsentSelector', Q('d2shareType').value < 8); } + QV('d2httpPortSelector', Q('d2shareType').value == 8); + QV('d2httpsPortSelector', Q('d2shareType').value == 9); QV('d2modenow', Q('d2timeRange').value == 0); QV('d2moderange', Q('d2timeRange').value == 1); QV('d2moderecurring', Q('d2timeRange').value >= 2); var ok = true; + if (Q('d2shareType').value == 8) { var port = parseInt(Q('d2httpPort').value); if ((Q('d2httpPort').value != port) || (port < 1) || (port > 65535)) { ok = false; } } + if (Q('d2shareType').value == 9) { var port = parseInt(Q('d2httpsPort').value); if ((Q('d2httpsPort').value != port) || (port < 1) || (port > 65535)) { ok = false; } } if (Q('d2inviteName').value.trim().length == 0) { ok = false; } QE('idx_dlgOkButton', ok); } function showShareDeviceEx(b, tag) { var consent = 0, p = parseInt(Q('d2shareType').value), viewOnly = false, q = 0; + if (p == 8) { meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: 8, expire: parseInt(Q('d2inviteExpire').value), port: parseInt(Q('d2httpPort').value), consent: 0 }); return; } + if (p == 9) { meshserver.send({ action: 'createDeviceShareLink', nodeid: currentNode._id, guestname: Q('d2inviteName').value.trim(), p: 16, expire: parseInt(Q('d2inviteExpire').value), port: parseInt(Q('d2httpsPort').value), consent: 0 }); return; } if (p == 3) { viewOnly = true; } - var q = [0, 1, 2, 2, 4, 6, 5, 7][p]; // Protocol flags: 1 = Terminal, 2 = Desktop, 4 = Files. + var q = [0, 1, 2, 2, 4, 6, 5, 7][p]; // Protocol flags: 1 = Terminal, 2 = Desktop, 4 = Files, 8 = HTTP, 16 = HTTPS. if (q & 1) { consent |= 0x0002; // Terminal notify @@ -12641,7 +12656,7 @@ var dshare = deviceShares[i], trash = ''; if (dshare.url != null) { trash += ' '; } trash += ''; - var type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Files", "Terminal + Files", "Desktop + Files", "Desktop + Terminal + Files"][dshare.p]; + var type = ''; if (dshare.p <= 7) { type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Files", "Terminal + Files", "Desktop + Files", "Desktop + Terminal + Files"][dshare.p]; } else if (dshare.p == 8) { type = "HTTP/" + dshare.port; } else if (dshare.p == 16) { type = "HTTPS/" + dshare.port; } var details = type; if ((dshare.startTime != null) && (dshare.expireTime != null)) { details = format("{0}, {1} to {2}", type, printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); } if ((dshare.startTime != null) && (dshare.duration != null)) { diff --git a/webrelayserver.js b/webrelayserver.js index e9f70353..07aa3349 100644 --- a/webrelayserver.js +++ b/webrelayserver.js @@ -124,8 +124,11 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, return next(); } else { // If this is a normal request (GET, POST, etc) handle it here - if ((req.session.userid != null) && (req.session.x != null) && (parent.webserver.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) { - var relaySession = relaySessions[req.session.userid + '/' + req.session.x]; + var webSessionId = null; + if ((req.session.userid != null) && (req.session.x != null)) { webSessionId = req.session.userid + '/' + req.session.x; } + else if (req.session.z != null) { webSessionId = req.session.z; } + if ((webSessionId != null) && (parent.webserver.destroyedSessions[webSessionId] == null)) { + var relaySession = relaySessions[webSessionId]; if (relaySession != null) { // The web relay session is valid, use it relaySession.handleRequest(req, res); @@ -157,8 +160,11 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, // Handle incoming web socket calls obj.app.ws('/*', function (ws, req) { - if ((req.session.userid != null) && (req.session.x != null) && (parent.webserver.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) { - var relaySession = relaySessions[req.session.userid + '/' + req.session.x]; + var webSessionId = null; + if ((req.session.userid != null) && (req.session.x != null)) { webSessionId = req.session.userid + '/' + req.session.x; } + else if (req.session.z != null) { webSessionId = req.session.z; } + if ((webSessionId != null) && (parent.webserver.destroyedSessions[webSessionId] == null)) { + var relaySession = relaySessions[webSessionId]; if (relaySession != null) { // The multi-tunnel session is valid, use it relaySession.handleWebSocket(ws, req); @@ -178,55 +184,85 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, parent.debug('webrelay', 'webRelaySetup'); // Decode the relay cookie - if (req.query.c != null) { - // Decode and check if this relay cookie is valid - const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey); - if ((urlCookie != null) && (urlCookie.ruserid != null) && (urlCookie.x != null) && (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] == null)) { - if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing - if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing - } + if (req.query.c == null) { res.sendStatus(404); return; } + + // Decode and check if this relay cookie is valid + var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire; + const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey); + if (urlCookie == null) { res.sendStatus(404); return; } + + // Decode the incomign cookie + if ((urlCookie.ruserid != null) && (urlCookie.x != null)) { + if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; } + + // This is a standard user, figure out what our web relay will be. + if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing + if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing + if (req.session.z) { delete req.session.z; } // Clear the web relay guest session + userid = req.session.userid; + domainid = userid.split('/')[1]; + domain = parent.config.domains[domainid]; + nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n); + addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1'; + port = parseInt(req.query.p); + appid = parseInt(req.query.appid); + webSessionId = req.session.userid + '/' + req.session.x; + + // Check that all the required arguments are present + if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; } + } else if (urlCookie.r == 8) { + // This is a guest user, figure out what our web relay will be. + userid = urlCookie.userid; + domainid = userid.split('/')[1]; + domain = parent.config.domains[domainid]; + nodeid = urlCookie.nid; + addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1'; + port = urlCookie.port; + appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS + webSessionId = userid + '/' + urlCookie.pid; + if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid + if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid + if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session + expire = urlCookie.expire; } - // Check that all the required arguments are present - if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[req.session.userid + '/' + req.session.x] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; } - - // Get the user and domain information - const userid = req.session.userid; - const domainid = userid.split('/')[1]; - const domain = parent.config.domains[domainid]; - const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n); - const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1'; - const port = parseInt(req.query.p); - const appid = parseInt(req.query.appid); + // No session identifier was setup, exit now + if (webSessionId == null) { res.sendStatus(404); return; } // Check to see if we already have a multi-relay session that matches exactly this device and port for this user - const xrelaySession = relaySessions[req.session.userid + '/' + req.session.x]; + const xrelaySession = relaySessions[webSessionId]; if ((xrelaySession != null) && (xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) { // We found an exact match, we are all setup already, redirect to root res.redirect('/'); return; } - // There is a relay session, but it's not correct, close it. - if (xrelaySession != null) { xrelaySession.close(); delete relaySessions[req.session.userid + '/' + req.session.x]; } + // Check that the user has rights to access this device + parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) { + // If there is no remote control rights, reject this web relay + if ((rights & 8) == 0) { res.sendStatus(404); return; } - // Create a web relay session - const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySession); - relaySession.onclose = function (sessionId) { - // Remove the relay session - delete relaySessions[sessionId]; - // If there are not more relay sessions, clear the cleanup timer - if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; } - } + // There is a relay session, but it's not correct, close it. + if (xrelaySession != null) { xrelaySession.close(); delete relaySessions[webSessionId]; } - // Set the multi-tunnel session - relaySessions[userid + '/' + req.session.x] = relaySession; + // Create a web relay session + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySession, expire); + relaySession.onclose = function (sessionId) { + // Remove the relay session + delete relaySessions[sessionId]; + // If there are not more relay sessions, clear the cleanup timer + if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; } + } - // Setup the cleanup timer if needed - if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); } + // Set the multi-tunnel session + relaySessions[webSessionId] = relaySession; - // Redirect to root - res.redirect('/'); + // Setup the cleanup timer if needed + if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); } + + // Redirect to root + res.redirect('/'); + }); }); } diff --git a/webserver.js b/webserver.js index 05f65e27..d4d8d54b 100644 --- a/webserver.js +++ b/webserver.js @@ -3848,7 +3848,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } // Generate an old style cookie from the information in the database - var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey }; + var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey ? doc.extrakey : null, port: doc.port }; if (doc.userid) { cookie.uid = doc.userid; } if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; } if (doc.startTime != null) { @@ -3870,7 +3870,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Check the public id obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) { - // Check if any desktop sharing links are present, expire message. + // Check if any sharing links are present, expire message. if ((err != null) || (docs.length == 0)) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } // Search for the device share public identifier, expire message. @@ -3886,22 +3886,43 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Check the start time, not yet valid message. if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } - // Looks good, let's create the outbound session cookies. - // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar. - const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, cf: c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo }; - if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; } - if (c.k != null) { authCookieData.k = c.k; } - const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey); + // If this is a web relay share, check if this feature is active + if ((c.p == 8) || (c.p == 16)) { + // This is a HTTP or HTTPS share + 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) { res.sendStatus(404); return; } - // Server features - var features2 = 0; - if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true) + // Create the authentication cookie + const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, r: 8, expire: c.expire, pid: c.pid, port: c.port }; + if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; } + const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey); - // Lets respond by sending out the desktop viewer. - var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified - parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".'); - res.set({ 'Cache-Control': 'no-store' }); - render(req, res, getRenderPage('sharing', req, domain), getRenderArgs({ authCookie: authCookie, authRelayCookie: '', domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), nodeid: c.nid, serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, expire: c.expire, viewOnly: (c.vo == 1) ? 1 : 0, nodeName: encodeURIComponent(node.name).replace(/'/g, '%27'), features: c.p, features2: features2 }, req, domain)); + // Redirect to a URL + var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req); + var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie; + if (c.addr != null) { url += '&addr=' + c.addr; } + if (c.pid != null) { url += '&relayid=' + c.pid; } + parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".'); + res.redirect(url); + } else { + // Looks good, let's create the outbound session cookies. + // This is a desktop, terminal or files share. We need to display the sharing page. + // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar. + const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, cf: c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo }; + if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; } + if (c.k != null) { authCookieData.k = c.k; } + const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey); + + // Server features + var features2 = 0; + if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true) + + // Lets respond by sending out the desktop viewer. + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified + parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".'); + res.set({ 'Cache-Control': 'no-store' }); + render(req, res, getRenderPage('sharing', req, domain), getRenderArgs({ authCookie: authCookie, authRelayCookie: '', domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), nodeid: c.nid, serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, expire: c.expire, viewOnly: (c.vo == 1) ? 1 : 0, nodeName: encodeURIComponent(node.name).replace(/'/g, '%27'), features: c.p, features2: features2 }, req, domain)); + } }); }); } @@ -6549,32 +6570,56 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF parent.debug('web', 'webRelaySetup'); // Decode the relay cookie - if (req.query.c != null) { - // Decode and check if this relay cookie is valid - const urlCookie = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey); - if ((urlCookie != null) && (urlCookie.ruserid != null) && (urlCookie.x != null)) { - if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing - if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing - } + if (req.query.c == null) { res.sendStatus(404); return; } + + // Decode and check if this relay cookie is valid + var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire; + const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey); + if (urlCookie == null) { res.sendStatus(404); return; } + + // Decode the incomign cookie + if ((urlCookie.ruserid != null) && (urlCookie.x != null)) { + if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; } + + // This is a standard user, figure out what our web relay will be. + if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing + if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing + if (req.session.z) { delete req.session.z; } // Clear the web relay guest session + userid = req.session.userid; + domainid = userid.split('/')[1]; + domain = parent.config.domains[domainid]; + nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n); + addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1'; + port = parseInt(req.query.p); + appid = parseInt(req.query.appid); + webSessionId = req.session.userid + '/' + req.session.x; + + // Check that all the required arguments are present + if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; } + } else if (urlCookie.r == 8) { + // This is a guest user, figure out what our web relay will be. + userid = urlCookie.userid; + domainid = userid.split('/')[1]; + domain = parent.config.domains[domainid]; + nodeid = urlCookie.nid; + addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1'; + port = urlCookie.port; + appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS + webSessionId = userid + '/' + urlCookie.pid; + if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid + if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid + if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session + expire = urlCookie.expire; } - // Check that all the required arguments are present - if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || ((obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null)) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; } - - // Get the user and domain information - const userid = req.session.userid; - const domainid = userid.split('/')[1]; - const domain = parent.config.domains[domainid]; - const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n); - const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1'; - const port = parseInt(req.query.p); - const appid = parseInt(req.query.appid); + // No session identifier was setup, exit now + if (webSessionId == null) { res.sendStatus(404); return; } // Check that we have an exact session on any of the relay DNS names var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost; for (var hostIndex in obj.args.relaydns) { const host = obj.args.relaydns[hostIndex]; - xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + host; + xrelaySessionId = webSessionId + '/' + host; xrelaySession = webRelaySessions[xrelaySessionId]; if (xrelaySession == null) { // We found an unused hostname, save this as it could be useful. @@ -6609,49 +6654,55 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } } - // Check if there is a free relay DNS name we can use - var selectedHost = null; - if (freeRelayHost != null) { - // There is a free one, use it. - selectedHost = freeRelayHost; - } else { - // No free ones, close the oldest one - selectedHost = oldestRelayHost; - } - xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + selectedHost; + // Check that the user has rights to access this device + parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) { + // If there is no remote control rights, reject this web relay + if ((rights & 8) == 0) { res.sendStatus(404); return; } - if (selectedHost == req.hostname) { - // If this web relay session id is not free, close it now - xrelaySession = webRelaySessions[xrelaySessionId]; - if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; } - - // Create a web relay session - const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId); - relaySession.onclose = function (sessionId) { - // Remove the relay session - delete webRelaySessions[sessionId]; - // If there are not more relay sessions, clear the cleanup timer - if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; } - } - - // Set the multi-tunnel session - webRelaySessions[xrelaySessionId] = relaySession; - - // Setup the cleanup timer if needed - if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); } - - // Redirect to root. - res.redirect('/'); - } else { - if (req.query.noredirect != null) { - // No redirects allowed, fail here. This is important to make sure there is no redirect cascades - res.sendStatus(404); + // Check if there is a free relay DNS name we can use + var selectedHost = null; + if (freeRelayHost != null) { + // There is a free one, use it. + selectedHost = freeRelayHost; } else { - // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name. - const httpport = ((args.aliasport != null) ? args.aliasport : args.port); - res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1'); + // No free ones, close the oldest one + selectedHost = oldestRelayHost; } - } + xrelaySessionId = webSessionId + '/' + selectedHost; + + if (selectedHost == req.hostname) { + // If this web relay session id is not free, close it now + xrelaySession = webRelaySessions[xrelaySessionId]; + if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; } + + // Create a web relay session + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire); + relaySession.onclose = function (sessionId) { + // Remove the relay session + delete webRelaySessions[sessionId]; + // If there are not more relay sessions, clear the cleanup timer + if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; } + } + + // Set the multi-tunnel session + webRelaySessions[xrelaySessionId] = relaySession; + + // Setup the cleanup timer if needed + if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); } + + // Redirect to root. + res.redirect('/'); + } else { + if (req.query.noredirect != null) { + // No redirects allowed, fail here. This is important to make sure there is no redirect cascades + res.sendStatus(404); + } else { + // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name. + const httpport = ((args.aliasport != null) ? args.aliasport : args.port); + res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1'); + } + } + }); }); // Handle all incoming requests as web relays @@ -6956,8 +7007,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Handle an incoming request as a web relay function handleWebRelayRequest(req, res) { - if ((req.session.userid != null) && (req.session.x != null) && (obj.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) { - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x + '/' + req.hostname]; + var webRelaySessionId = null; + if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; } + else if (req.session.z != null) { webRelaySessionId = req.session.z; } + if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) { + var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname]; if (relaySession != null) { // The web relay session is valid, use it relaySession.handleRequest(req, res); @@ -6973,8 +7027,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Handle an incoming websocket connection as a web relay function handleWebRelayWebSocket(ws, req) { - if ((req.session.userid != null) && (req.session.x != null) && (obj.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) { - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x + '/' + req.hostname]; + var webRelaySessionId = null; + if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; } + else if (req.session.z != null) { webRelaySessionId = req.session.z; } + if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) { + var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname]; if (relaySession != null) { // The multi-tunnel session is valid, use it relaySession.handleWebSocket(ws, req);