Added guest web sharing of HTTP/HTTPS (#4413)

Ylian Saint-Hilaire committed Aug 25, 2022 at 20:11 UTC 5d7fabfc2164d025436b899776a658fc80dc7111
9 files changed +1012 -588
agents/MeshCmd-signed.exe
Binary files a/agents/MeshCmd-signed.exe and b/agents/MeshCmd-signed.exe differ
agents/MeshCmd64-signed.exe
Binary files a/agents/MeshCmd64-signed.exe and b/agents/MeshCmd64-signed.exe differ
agents/meshcore.js
+771 -463
@@ -23,8 +23,7 @@ if (process.platform == 'win32' && require('user-sessions').getDomain == null) {
23 };
24 }
25
26 -// NOTE: This seems to cause big problems, don't enable the debugger in the server's meshcore.
27 -//attachDebugger({ webport: 9999, wait: 1 }).then(function (prt) { console.log('Point Browser for Debug to port: ' + prt); });
26 +var promise = require('promise');
27
28 // Mesh Rights
29 var MNG_ERROR = 65;
@@ -1043,6 +1042,35 @@ function server_check_consentTimer(id) {
1042 return false;
1043 }
1044
1045 +function tunnel_finalized()
1046 +{
1047 + console.info1('Tunnel Request Finalized');
1048 +}
1049 +function tunnel_checkServerIdentity(certs)
1050 +{
1051 + /*
1052 + try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); }
1053 + try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); }
1054 + try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); }
1055 + try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); }
1056 + */
1057 +
1058 + // Check if this is an old agent, no certificate checks are possible in this situation. Display a warning.
1059 + 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; }
1060 +
1061 + // If the tunnel certificate matches the control channel certificate, accept the connection
1062 + if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; // Control channel certificate matches using full cert hash
1063 + if ((certs[0].fingerprint != null) && (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint)) return; // Control channel certificate matches using public key hash
1064 +
1065 + // Check that the certificate is the one expected by the server, fail if not.
1066 + if ((tunnel_checkServerIdentity.servertlshash != null) && (tunnel_checkServerIdentity.servertlshash.toLowerCase() != certs[0].digest.split(':').join('').toLowerCase())) { throw new Error('BadCert') }
1067 +}
1068 +
1069 +function tunnel_onError()
1070 +{
1071 + sendConsoleText("ERROR: Unable to connect relay tunnel to: " + this.url + ", " + JSON.stringify(e));
1072 +}
1073 +
1074 // Handle a mesh agent command
1075 function handleServerCommand(data) {
1076 if (typeof data == 'object') {
@@ -1062,7 +1090,8 @@ function handleServerCommand(data) {
1090 }
1091 break;
1092 }
1065 - case 'tunnel': {
1093 + case 'tunnel':
1094 + {
1095 if (data.value != null) { // Process a new tunnel connection request
1096 // Create a new tunnel object
1097 var xurl = getServerTargetUrlEx(data.value);
@@ -1074,31 +1103,15 @@ function handleServerCommand(data) {
1103
1104 // Perform manual server TLS certificate checking based on the certificate hash given by the server.
1105 woptions.rejectUnauthorized = 0;
1077 - woptions.checkServerIdentity = function checkServerIdentity(certs) {
1078 - /*
1079 - try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); }
1080 - try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); }
1081 - try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); }
1082 - try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); }
1083 - */
1084 -
1085 - // Check if this is an old agent, no certificate checks are possible in this situation. Display a warning.
1086 - 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; }
1087 -
1088 - // If the tunnel certificate matches the control channel certificate, accept the connection
1089 - if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; // Control channel certificate matches using full cert hash
1090 - if ((certs[0].fingerprint != null) && (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint)) return; // Control channel certificate matches using public key hash
1091 -
1092 - // Check that the certificate is the one expected by the server, fail if not.
1093 - if ((checkServerIdentity.servertlshash != null) && (checkServerIdentity.servertlshash.toLowerCase() != certs[0].digest.split(':').join('').toLowerCase())) { throw new Error('BadCert') }
1094 - }
1106 + woptions.checkServerIdentity = tunnel_checkServerIdentity;
1107 woptions.checkServerIdentity.servertlshash = data.servertlshash;
1108
1109 //sendConsoleText(JSON.stringify(woptions));
1110 //sendConsoleText('TUNNEL: ' + JSON.stringify(data, null, 2));
1111 +
1112 var tunnel = http.request(woptions);
1113 tunnel.upgrade = onTunnelUpgrade;
1101 - tunnel.on('error', function (e) { sendConsoleText("ERROR: Unable to connect relay tunnel to: " + this.url + ", " + JSON.stringify(e)); });
1114 + tunnel.on('error', tunnel_onError);
1115 tunnel.sessionid = data.sessionid;
1116 tunnel.rights = data.rights;
1117 tunnel.consent = data.consent;
@@ -1122,11 +1135,13 @@ function handleServerCommand(data) {
1135 tunnel.tcpport = data.tcpport;
1136 tunnel.udpaddr = data.udpaddr;
1137 tunnel.udpport = data.udpport;
1125 - tunnel.end();
1138 +
1139 // Put the tunnel in the tunnels list
1140 var index = nextTunnelIndex++;
1141 tunnel.index = index;
1142 tunnels[index] = tunnel;
1143 + tunnel.once('~', tunnel_finalized);
1144 + tunnel.end();
1145
1146 //sendConsoleText('New tunnel connection #' + index + ': ' + tunnel.url + ', rights: ' + tunnel.rights, data.sessionid);
1147 }
@@ -1355,7 +1370,9 @@ function handleServerCommand(data) {
1370 this._dispatcher.on('connection', function (c) {
1371 this._c = c;
1372 this._c.root = this.parent;
1358 - this._c.on('end', function () {
1373 + this._c.on('end', function ()
1374 + {
1375 + this.root._dispatcher.close();
1376 this.root._dispatcher = null;
1377 this.root = null;
1378 mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
@@ -1840,20 +1857,34 @@ function getDirectoryInfo(reqpath) {
1857 return response;
1858 }
1859
1860 +function tunnel_s_finalized()
1861 +{
1862 + console.info1('Tunnel Socket Finalized');
1863 +}
1864 +
1865 +
1866 +function tunnel_onIdleTimeout()
1867 +{
1868 + this.ping();
1869 + this.setTimeout(require('MeshAgent').idleTimeout * 1000);
1870 +}
1871 +
1872 // Tunnel callback operations
1844 -function onTunnelUpgrade(response, s, head) {
1873 +function onTunnelUpgrade(response, s, head)
1874 +{
1875 +
1876 this.s = s;
1877 + s.once('~', tunnel_s_finalized);
1878 s.httprequest = this;
1879 s.end = onTunnelClosed;
1880 s.tunnel = this;
1881 s.descriptorMetadata = "MeshAgent_relayTunnel";
1882
1851 - if (require('MeshAgent').idleTimeout != null) {
1883 +
1884 + if (require('MeshAgent').idleTimeout != null)
1885 + {
1886 s.setTimeout(require('MeshAgent').idleTimeout * 1000);
1853 - s.on('timeout', function () {
1854 - this.ping();
1855 - this.setTimeout(require('MeshAgent').idleTimeout * 1000);
1856 - });
1887 + s.on('timeout', tunnel_onIdleTimeout);
1888 }
1889
1890 //sendConsoleText('onTunnelUpgrade - ' + this.tcpport + ' - ' + this.udpport);
@@ -1937,7 +1968,25 @@ function onTcpRelayServerTunnelData(data) {
1968 }
1969 }
1970
1940 -function onTunnelClosed() {
1971 +function onTunnelClosed()
1972 +{
1973 + if (this.httprequest._dispatcher != null && this.httprequest.term == null)
1974 + {
1975 + // 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
1976 + if (this.httprequest._dispatcher.close) { this.httprequest._dispatcher.close(); }
1977 + this.httprequest._dispatcher = null;
1978 + }
1979 +
1980 + if (this.tunnel)
1981 + {
1982 + if (tunnels[this.httprequest.index] == null)
1983 + {
1984 + this.tunnel.s = null;
1985 + this.tunnel = null;
1986 + return;
1987 + }
1988 + }
1989 +
1990 var tunnel = tunnels[this.httprequest.index];
1991 if (tunnel == null) return; // Stop duplicate calls.
1992
@@ -1987,7 +2036,7 @@ function onTunnelClosed() {
2036 } catch (ex) { }
2037
2038 //sendConsoleText("Tunnel #" + this.httprequest.index + " closed. Sent -> " + this.bytesSent_uncompressed + ' bytes (uncompressed), ' + this.bytesSent_actual + ' bytes (actual), ' + this.bytesSent_ratio + '% compression', this.httprequest.sessionid);
1990 - if (this.httprequest.index) { delete tunnels[this.httprequest.index]; }
2039 +
2040
2041 /*
2042 // Close the watcher if required
@@ -2014,11 +2063,523 @@ function onTunnelClosed() {
2063 }
2064
2065 // Clean up WebSocket
2066 + delete tunnels[this.httprequest.index];
2067 + tunnel = null;
2068 + this.tunnel.s = null;
2069 + this.tunnel = null;
2070 this.removeAllListeners('data');
2071 }
2072 function onTunnelSendOk() { /*sendConsoleText("Tunnel #" + this.index + " SendOK.", this.sessionid);*/ }
2020 -function onTunnelData(data) {
2021 - //console.log("OnTunnelData");
2073 +
2074 +function terminal_onconnection (c)
2075 +{
2076 + if (this.httprequest.connectionPromise.completed)
2077 + {
2078 + c.end();
2079 + }
2080 + else
2081 + {
2082 + this.httprequest.connectionPromise._res(c);
2083 + }
2084 +}
2085 +function terminal_user_onconnection(c)
2086 +{
2087 + console.info1('completed-2: ' + this.connectionPromise.completed);
2088 +
2089 + if (this.connectionPromise.completed)
2090 + {
2091 + c.end();
2092 + }
2093 + else
2094 + {
2095 + this.connectionPromise._res(c);
2096 + }
2097 +}
2098 +function terminal_stderr_ondata(c)
2099 +{
2100 + this.stdout.write(c);
2101 +}
2102 +function terminal_onend()
2103 +{
2104 + this.httprequest.process.kill();
2105 +}
2106 +
2107 +function terminal_onexit()
2108 +{
2109 + this.tunnel.end();
2110 +}
2111 +function terminal_onfinalized()
2112 +{
2113 + this.httprequest = null;
2114 + console.info1('Dispatcher Finalized');
2115 +}
2116 +function terminal_end()
2117 +{
2118 + if (this.httprequest == null) { return; }
2119 + if (this.httprequest.tpromise._consent) { this.httprequest.tpromise._consent.close(); }
2120 + if (this.httprequest.connectionPromise) { this.httprequest.connectionPromise._rej('Closed'); }
2121 +
2122 + // Remove the terminal session to the count to update the server
2123 + if (this.httprequest.userid != null)
2124 + {
2125 + var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest);
2126 + if (tunnelUserCount.terminal[userid] != null) { tunnelUserCount.terminal[userid]--; if (tunnelUserCount.terminal[userid] <= 0) { delete tunnelUserCount.terminal[userid]; } }
2127 + try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { }
2128 + broadcastSessionsToRegisteredApps();
2129 + }
2130 +
2131 + if (process.platform == 'win32')
2132 + {
2133 + // Unpipe the web socket
2134 + this.unpipe(this.httprequest._term);
2135 + if (this.httprequest._term) { this.httprequest._term.unpipe(this); }
2136 +
2137 + // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends).
2138 + if (this.rtcchannel)
2139 + {
2140 + this.rtcchannel.unpipe(this.httprequest._term);
2141 + if (this.httprequest._term) { this.httprequest._term.unpipe(this.rtcchannel); }
2142 + }
2143 +
2144 + // Clean up
2145 + if (this.httprequest._term) { this.httprequest._term.end(); }
2146 + this.httprequest._term = null;
2147 + this.httprequest._dispatcher = null;
2148 + }
2149 +
2150 + this.httprequest = null;
2151 +
2152 +}
2153 +
2154 +function terminal_promise_connection_rejected(e)
2155 +{
2156 + // FAILED to connect terminal
2157 + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2158 + this.ws.end();
2159 +}
2160 +
2161 +function terminal_promise_connection_resolved(term)
2162 +{
2163 + this._internal.completedArgs = [];
2164 +
2165 + // SUCCESS
2166 + var stdoutstream;
2167 + var stdinstream;
2168 + if (process.platform == 'win32')
2169 + {
2170 + this.ws.httprequest._term = term;
2171 + this.ws.httprequest._term.tunnel = this.ws;
2172 + stdoutstream = stdinstream = term;
2173 + }
2174 + else
2175 + {
2176 + term.descriptorMetadata = 'Remote Terminal';
2177 + this.ws.httprequest.process = term;
2178 + this.ws.httprequest.process.tunnel = this.ws;
2179 + term.stderr.stdout = term.stdout;
2180 + term.stderr.on('data', terminal_stderr_ondata);
2181 + stdoutstream = term.stdout;
2182 + stdinstream = term.stdin;
2183 + this.ws.prependListener('end', terminal_onend);
2184 + term.prependListener('exit', terminal_onexit);
2185 + }
2186 +
2187 + this.ws.removeAllListeners('data');
2188 + this.ws.on('data', onTunnelControlData);
2189 +
2190 + stdoutstream.pipe(this.ws, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text.
2191 + this.ws.pipe(stdinstream, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text.
2192 +
2193 + // Add the terminal session to the count to update the server
2194 + if (this.ws.httprequest.userid != null)
2195 + {
2196 + var userid = getUserIdAndGuestNameFromHttpRequest(this.ws.httprequest);
2197 + if (tunnelUserCount.terminal[userid] == null) { tunnelUserCount.terminal[userid] = 1; } else { tunnelUserCount.terminal[userid]++; }
2198 + try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { }
2199 + broadcastSessionsToRegisteredApps();
2200 + }
2201 +
2202 + // Toast Notification, if required
2203 + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 2))
2204 + {
2205 + // User Notifications is required
2206 + var notifyMessage = currentTranslation['terminalNotify'].replace('{0}', this.ws.httprequest.username);
2207 + var notifyTitle = "MeshCentral";
2208 + if (this.ws.httprequest.soptions != null)
2209 + {
2210 + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; }
2211 + 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); }
2212 + }
2213 + try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { }
2214 + }
2215 + this.ws = null;
2216 +}
2217 +function terminal_promise_consent_rejected(e)
2218 +{
2219 + // DO NOT start terminal
2220 + this.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2221 + this.that.end();
2222 +
2223 + this.that = null;
2224 + this.httprequest = null;
2225 +}
2226 +function promise_init(res, rej) { this._res = res; this._rej = rej; }
2227 +function terminal_userpromise_resolved(u)
2228 +{
2229 +
2230 + var that = this.that;
2231 + if (u.Active.length > 0)
2232 + {
2233 + var tmp;
2234 + var username = '"' + u.Active[0].Domain + '\\' + u.Active[0].Username + '"';
2235 +
2236 +
2237 + if (require('win-virtual-terminal').supported)
2238 + {
2239 + // ConPTY PseudoTerminal
2240 + 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] } });
2241 + }
2242 + else
2243 + {
2244 + // Legacy Terminal
2245 + 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] } });
2246 + }
2247 + that.httprequest._dispatcher = tmp;
2248 + that.httprequest._dispatcher.connectionPromise = that.httprequest.connectionPromise;
2249 + that.httprequest._dispatcher.on('connection', terminal_user_onconnection);
2250 + that.httprequest._dispatcher.on('~', terminal_onfinalized);
2251 + }
2252 + this.that = null;
2253 + that = null;
2254 +}
2255 +
2256 +function terminal_promise_consent_resolved()
2257 +{
2258 + this.httprequest.connectionPromise = new promise(promise_init);
2259 + this.httprequest.connectionPromise.ws = this.that;
2260 +
2261 + // Start Terminal
2262 + if (process.platform == 'win32')
2263 + {
2264 + try
2265 + {
2266 + var cols = 80, rows = 25;
2267 + if (this.httprequest.xoptions)
2268 + {
2269 + if (this.httprequest.xoptions.rows) { rows = this.httprequest.xoptions.rows; }
2270 + if (this.httprequest.xoptions.cols) { cols = this.httprequest.xoptions.cols; }
2271 + }
2272 +
2273 + if ((this.httprequest.protocol == 1) || (this.httprequest.protocol == 6))
2274 + {
2275 + // Admin Terminal
2276 + if (require('win-virtual-terminal').supported)
2277 + {
2278 + // ConPTY PseudoTerminal
2279 + // this.httprequest._term = require('win-virtual-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](80, 25);
2280 +
2281 + // The above line is commented out, because there is a bug with ClosePseudoConsole() API, so this is the workaround
2282 + 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] } });
2283 + this.httprequest._dispatcher.httprequest = this.httprequest;
2284 + this.httprequest._dispatcher.on('connection', terminal_onconnection);
2285 + this.httprequest._dispatcher.on('~', terminal_onfinalized);
2286 + }
2287 + else
2288 + {
2289 + // Legacy Terminal
2290 + this.httprequest.connectionPromise._res(require('win-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](cols, rows));
2291 + }
2292 + }
2293 + else
2294 + {
2295 + // Logged in user
2296 + var userPromise = require('user-sessions').enumerateUsers();
2297 + userPromise.that = this;
2298 + userPromise.cols = cols;
2299 + userPromise.rows = rows;
2300 + userPromise.then(terminal_userpromise_resolved);
2301 + }
2302 + } catch (ex)
2303 + {
2304 + this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString());
2305 + }
2306 + }
2307 + else
2308 + {
2309 + try
2310 + {
2311 + var bash = fs.existsSync('/bin/bash') ? '/bin/bash' : false;
2312 + var sh = fs.existsSync('/bin/sh') ? '/bin/sh' : false;
2313 + var login = process.platform == 'linux' ? '/bin/login' : '/usr/bin/login';
2314 +
2315 + var env = { HISTCONTROL: 'ignoreboth' };
2316 + if (process.env['LANG']) { env['LANG'] = process.env['LANG']; }
2317 + if (process.env['PATH']) { env['PATH'] = process.env['PATH']; }
2318 + if (this.httprequest.xoptions)
2319 + {
2320 + if (this.httprequest.xoptions.rows) { env.LINES = ('' + this.httprequest.xoptions.rows); }
2321 + if (this.httprequest.xoptions.cols) { env.COLUMNS = ('' + this.httprequest.xoptions.cols); }
2322 + }
2323 + var options = { type: childProcess.SpawnTypes.TERM, uid: (this.httprequest.protocol == 8) ? require('user-sessions').consoleUid() : null, env: env };
2324 + if (this.httprequest.xoptions && this.httprequest.xoptions.requireLogin)
2325 + {
2326 + if (!require('fs').existsSync(login)) { throw ('Unable to spawn login process'); }
2327 + this.httprequest.connectionPromise._res(childProcess.execFile(login, ['login'], options)); // Start login shell
2328 + }
2329 + else if (bash)
2330 + {
2331 + var p = childProcess.execFile(bash, ['bash'], options); // Start bash
2332 + // Spaces at the beginning of lines are needed to hide commands from the command history
2333 + if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string'))
2334 + {
2335 + if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); }
2336 + } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); }
2337 + this.httprequest.connectionPromise._res(p);
2338 + }
2339 + else if (sh)
2340 + {
2341 + var p = childProcess.execFile(sh, ['sh'], options); // Start sh
2342 + // Spaces at the beginning of lines are needed to hide commands from the command history
2343 + if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string'))
2344 + {
2345 + if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); }
2346 + } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); }
2347 + this.httprequest.connectionPromise._res(p);
2348 + }
2349 + else
2350 + {
2351 + this.httprequest.connectionPromise._rej('Failed to start remote terminal session, no shell found');
2352 + }
2353 + } catch (ex)
2354 + {
2355 + this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString());
2356 + }
2357 + }
2358 +
2359 + this.httprequest.connectionPromise.then(terminal_promise_connection_resolved, terminal_promise_connection_rejected);
2360 + this.that = null;
2361 + this.httprequest = null;
2362 +}
2363 +function tunnel_kvm_end()
2364 +{
2365 + --this.desktop.kvm.connectionCount;
2366 +
2367 + // Remove ourself from the list of remote desktop session
2368 + var i = this.desktop.kvm.tunnels.indexOf(this);
2369 + if (i >= 0) { this.desktop.kvm.tunnels.splice(i, 1); }
2370 +
2371 + // Send a metadata update to all desktop sessions
2372 + var users = {};
2373 + if (this.httprequest.desktop.kvm.tunnels != null)
2374 + {
2375 + for (var i in this.httprequest.desktop.kvm.tunnels)
2376 + {
2377 + try
2378 + {
2379 + var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest.desktop.kvm.tunnels[i].httprequest);
2380 + if (users[userid] == null) { users[userid] = 1; } else { users[userid]++; }
2381 + } catch (ex) { sendConsoleText(ex); }
2382 + }
2383 + for (var i in this.httprequest.desktop.kvm.tunnels)
2384 + {
2385 + try { this.httprequest.desktop.kvm.tunnels[i].write(JSON.stringify({ ctrlChannel: '102938', type: 'metadata', users: users })); } catch (ex) { }
2386 + }
2387 + tunnelUserCount.desktop = users;
2388 + try { mesh.SendCommand({ action: 'sessions', type: 'kvm', value: users }); } catch (ex) { }
2389 + broadcastSessionsToRegisteredApps();
2390 + }
2391 +
2392 + // Unpipe the web socket
2393 + try
2394 + {
2395 + this.unpipe(this.httprequest.desktop.kvm);
2396 + this.httprequest.desktop.kvm.unpipe(this);
2397 + } catch (ex) { }
2398 +
2399 + // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends).
2400 + if (this.rtcchannel)
2401 + {
2402 + try
2403 + {
2404 + this.rtcchannel.unpipe(this.httprequest.desktop.kvm);
2405 + this.httprequest.desktop.kvm.unpipe(this.rtcchannel);
2406 + }
2407 + catch (ex) { }
2408 + }
2409 +
2410 + // Place wallpaper back if needed
2411 + // TODO
2412 +
2413 + if (this.desktop.kvm.connectionCount == 0)
2414 + {
2415 + // Display a toast message. This may not be supported on all platforms.
2416 + // try { require('toaster').Toast('MeshCentral', 'Remote Desktop Control Ended.'); } catch (ex) { }
2417 +
2418 + this.httprequest.desktop.kvm.end();
2419 + if (this.httprequest.desktop.kvm.connectionBar)
2420 + {
2421 + this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2422 + this.httprequest.desktop.kvm.connectionBar.close();
2423 + this.httprequest.desktop.kvm.connectionBar = null;
2424 + }
2425 + } else
2426 + {
2427 + for (var i in this.httprequest.desktop.kvm.users)
2428 + {
2429 + if ((this.httprequest.desktop.kvm.users[i] == this.httprequest.username) && this.httprequest.desktop.kvm.connectionBar)
2430 + {
2431 + 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; } }
2432 + this.httprequest.desktop.kvm.users.splice(i, 1);
2433 + this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2434 + this.httprequest.desktop.kvm.connectionBar.close();
2435 + 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);
2436 + this.httprequest.desktop.kvm.connectionBar.httprequest = this.httprequest;
2437 + this.httprequest.desktop.kvm.connectionBar.on('close', function ()
2438 + {
2439 + MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest);
2440 + for (var i in this.httprequest.desktop.kvm._pipedStreams)
2441 + {
2442 + this.httprequest.desktop.kvm._pipedStreams[i].end();
2443 + }
2444 + this.httprequest.desktop.kvm.end();
2445 + });
2446 + break;
2447 + }
2448 + }
2449 + }
2450 +
2451 + if(this.httprequest.desktop.kvm.connectionBar)
2452 + {
2453 + console.info1('Setting ConnectionBar request to NULL');
2454 + this.httprequest.desktop.kvm.connectionBar.httprequest = null;
2455 + }
2456 +
2457 + this.httprequest = null;
2458 + this.desktop.tunnel = null;
2459 +}
2460 +
2461 +function kvm_tunnel_consentpromise_closehandler()
2462 +{
2463 + if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); }
2464 +}
2465 +
2466 +function kvm_consentpromise_rejected(e)
2467 +{
2468 + // User Consent Denied/Failed
2469 + this.ws._consentpromise = null;
2470 + MeshServerLogEx(34, null, "Failed to start remote desktop after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2471 + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2472 + this.ws = null;
2473 +}
2474 +function kvm_consentpromise_resolved(always)
2475 +{
2476 + if (always && process.platform=='win32') { server_set_consentTimer(this.ws.httprequest.userid); }
2477 +
2478 + // Success
2479 + this.ws._consentpromise = null;
2480 + MeshServerLogEx(30, null, "Starting remote desktop after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2481 + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null, msgid: 0 }));
2482 + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 1))
2483 + {
2484 + // User Notifications is required
2485 + var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.ws.httprequest.realname);
2486 + var notifyTitle = "MeshCentral";
2487 + if (this.ws.httprequest.soptions != null)
2488 + {
2489 + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; }
2490 + 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); }
2491 + }
2492 + try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { }
2493 + }
2494 + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 0x40))
2495 + {
2496 + // Connection Bar is required
2497 + if (this.ws.httprequest.desktop.kvm.connectionBar)
2498 + {
2499 + this.ws.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2500 + this.ws.httprequest.desktop.kvm.connectionBar.close();
2501 + }
2502 + try
2503 + {
2504 + 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);
2505 + MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2506 + } catch (ex)
2507 + {
2508 + if (process.platform != 'darwin')
2509 + {
2510 + MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or Not Supported (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2511 + }
2512 + }
2513 + if (this.ws.httprequest.desktop.kvm.connectionBar)
2514 + {
2515 + this.ws.httprequest.desktop.kvm.connectionBar.state =
2516 + {
2517 + userid: this.ws.httprequest.userid,
2518 + xuserid: this.ws.httprequest.xuserid,
2519 + username: this.ws.httprequest.username,
2520 + sessionid: this.ws.httprequest.sessionid,
2521 + remoteaddr: this.ws.httprequest.remoteaddr,
2522 + guestname: this.ws.httprequest.guestname,
2523 + desktop: this.ws.httprequest.desktop
2524 + };
2525 + this.ws.httprequest.desktop.kvm.connectionBar.on('close', function ()
2526 + {
2527 + MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.state.remoteaddr + ")", state);
2528 + for (var i in this.state.desktop.kvm._pipedStreams)
2529 + {
2530 + this.state.desktop.kvm._pipedStreams[i].end();
2531 + }
2532 + this.state.desktop.kvm.end();
2533 + });
2534 + }
2535 + }
2536 + this.ws.httprequest.desktop.kvm.pipe(this.ws, { dataTypeSkip: 1 });
2537 + if (this.ws.httprequest.autolock)
2538 + {
2539 + destopLockHelper_pipe(this.ws.httprequest);
2540 + }
2541 + this.ws.resume();
2542 + this.ws = null;
2543 +}
2544 +
2545 +function files_consentpromise_resolved(always)
2546 +{
2547 + if (always && process.platform == 'win32') { server_set_consentTimer(this.ws.httprequest.userid); }
2548 +
2549 + // Success
2550 + this.ws._consentpromise = null;
2551 + MeshServerLogEx(40, null, "Starting remote files after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2552 + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null }));
2553 + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 4))
2554 + {
2555 + // User Notifications is required
2556 + var notifyMessage = currentTranslation['fileNotify'].replace('{0}', this.ws.httprequest.realname);
2557 + var notifyTitle = "MeshCentral";
2558 + if (this.ws.httprequest.soptions != null)
2559 + {
2560 + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; }
2561 + 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); }
2562 + }
2563 + try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { }
2564 + }
2565 + this.ws.resume();
2566 + this.ws = null;
2567 +}
2568 +function files_consentpromise_rejected(e)
2569 +{
2570 + // User Consent Denied/Failed
2571 + this.ws._consentpromise = null;
2572 + MeshServerLogEx(41, null, "Failed to start remote files after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2573 + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2574 + this.ws = null;
2575 +}
2576 +function files_tunnel_endhandler()
2577 +{
2578 + if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); }
2579 +}
2580 +
2581 +function onTunnelData(data)
2582 +{
2583 //sendConsoleText('OnTunnelData, ' + data.length + ', ' + typeof data + ', ' + data);
2584
2585 // If this is upload data, save it to file
@@ -2080,7 +2641,8 @@ function onTunnelData(data) {
2641 //
2642
2643 // Check user access rights for terminal
2083 - if (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) == 0) || ((this.httprequest.rights != 0xFFFFFFFF) && ((this.httprequest.rights & MESHRIGHT_NOTERMINAL) != 0))) {
2644 + if (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) == 0) || ((this.httprequest.rights != 0xFFFFFFFF) && ((this.httprequest.rights & MESHRIGHT_NOTERMINAL) != 0)))
2645 + {
2646 // Disengage this tunnel, user does not have the rights to do this!!
2647 this.httprequest.protocol = 999999;
2648 this.httprequest.s.end();
@@ -2090,7 +2652,8 @@ function onTunnelData(data) {
2652
2653 this.descriptorMetadata = "Remote Terminal";
2654
2093 - if (process.platform == 'win32') {
2655 + if (process.platform == 'win32')
2656 + {
2657 if (!require('win-terminal').PowerShellCapable() && (this.httprequest.protocol == 6 || this.httprequest.protocol == 9)) {
2658 this.httprequest.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: 'PowerShell is not supported on this version of windows', msgid: 1 }));
2659 this.httprequest.s.end();
@@ -2099,53 +2662,29 @@ function onTunnelData(data) {
2662 }
2663
2664 var prom = require('promise');
2102 - this.httprequest.tpromise = new prom(function (res, rej) { this._res = res; this._rej = rej; });
2665 + this.httprequest.tpromise = new prom(promise_init);
2666 this.httprequest.tpromise.that = this;
2667 this.httprequest.tpromise.httprequest = this.httprequest;
2105 -
2106 - this.end = function () {
2107 - if (this.httprequest.tpromise._consent) { this.httprequest.tpromise._consent.close(); }
2108 - if (this.httprequest.connectionPromise) { this.httprequest.connectionPromise._rej('Closed'); }
2109 -
2110 - // Remove the terminal session to the count to update the server
2111 - if (this.httprequest.userid != null) {
2112 - var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest);
2113 - if (tunnelUserCount.terminal[userid] != null) { tunnelUserCount.terminal[userid]--; if (tunnelUserCount.terminal[userid] <= 0) { delete tunnelUserCount.terminal[userid]; } }
2114 - try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { }
2115 - broadcastSessionsToRegisteredApps();
2116 - }
2117 -
2118 - if (process.platform == 'win32') {
2119 - // Unpipe the web socket
2120 - this.unpipe(this.httprequest._term);
2121 - if (this.httprequest._term) { this.httprequest._term.unpipe(this); }
2122 -
2123 - // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends).
2124 - if (this.rtcchannel) {
2125 - this.rtcchannel.unpipe(this.httprequest._term);
2126 - if (this.httprequest._term) { this.httprequest._term.unpipe(this.rtcchannel); }
2127 - }
2128 -
2129 - // Clean up
2130 - if (this.httprequest._term) { this.httprequest._term.end(); }
2131 - this.httprequest._term = null;
2132 - }
2133 - };
2668 + this.end = terminal_end;
2669
2670 // Perform User-Consent if needed.
2136 - if (this.httprequest.consent && (this.httprequest.consent & 16)) {
2671 + if (this.httprequest.consent && (this.httprequest.consent & 16))
2672 + {
2673 this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
2674 var consentMessage = currentTranslation['terminalConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username);
2675 var consentTitle = 'MeshCentral';
2676
2141 - if (this.httprequest.soptions != null) {
2677 + if (this.httprequest.soptions != null)
2678 + {
2679 if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; }
2680 if (this.httprequest.soptions.consentMsgTerminal != null) { consentMessage = this.httprequest.soptions.consentMsgTerminal.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
2681 }
2145 - if (process.platform == 'win32') {
2682 + if (process.platform == 'win32')
2683 + {
2684 var enhanced = false;
2685 try { require('win-userconsent'); enhanced = true; } catch (ex) { }
2148 - if (enhanced) {
2686 + if (enhanced)
2687 + {
2688 var ipr = server_getUserImage(this.httprequest.userid);
2689 ipr.consentTitle = consentTitle;
2690 ipr.consentMessage = consentMessage;
@@ -2153,21 +2692,25 @@ function onTunnelData(data) {
2692 ipr.consentAutoAccept = this.httprequest.consentAutoAccept;
2693 ipr.username = this.httprequest.realname;
2694 ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
2156 - this.httprequest.tpromise._consent = ipr.then(function (img) {
2695 + this.httprequest.tpromise._consent = ipr.then(function (img)
2696 + {
2697 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 });
2698 this.__childPromise.close = this.consent.close.bind(this.consent);
2699 return (this.consent);
2700 });
2161 - } else {
2162 - this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.consentTimeout);
2701 + } else
2702 + {
2703 + this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout);
2704 }
2164 - } else {
2165 - this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.consentTimeout);
2705 + } else
2706 + {
2707 + this.httprequest.tpromise._consent = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout);
2708 }
2709 this.httprequest.tpromise._consent.retPromise = this.httprequest.tpromise;
2710 this.httprequest.tpromise._consent.then(
2169 - function (always) {
2170 - if (always) { server_set_consentTimer(this.retPromise.httprequest.userid); }
2711 + function (always)
2712 + {
2713 + if (always && process.platform == 'win32') { server_set_consentTimer(this.retPromise.httprequest.userid); }
2714
2715 // Success
2716 MeshServerLogEx(27, null, "Local user accepted remote terminal request (" + this.retPromise.httprequest.remoteaddr + ")", this.retPromise.that.httprequest);
@@ -2179,174 +2722,21 @@ function onTunnelData(data) {
2722 // Denied
2723 MeshServerLogEx(28, null, "Local user rejected remote terminal request (" + this.retPromise.that.httprequest.remoteaddr + ")", this.retPromise.that.httprequest);
2724 this.retPromise.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2725 + this.retPromise._consent = null;
2726 this.retPromise._rej(e.toString());
2727 });
2728 }
2185 - else {
2729 + else
2730 + {
2731 // User-Consent is not required, so just resolve this promise
2732 this.httprequest.tpromise._res();
2733 }
2734
2735
2191 - this.httprequest.tpromise.then(
2192 - function () {
2193 - this.httprequest.connectionPromise = new prom(function (res, rej) { this._res = res; this._rej = rej; });
2194 - this.httprequest.connectionPromise.ws = this.that;
2195 -
2196 - // Start Terminal
2197 - if (process.platform == 'win32') {
2198 - try {
2199 - var cols = 80, rows = 25;
2200 - if (this.httprequest.xoptions) {
2201 - if (this.httprequest.xoptions.rows) { rows = this.httprequest.xoptions.rows; }
2202 - if (this.httprequest.xoptions.cols) { cols = this.httprequest.xoptions.cols; }
2203 - }
2204 -
2205 - if ((this.httprequest.protocol == 1) || (this.httprequest.protocol == 6)) {
2206 - // Admin Terminal
2207 - if (require('win-virtual-terminal').supported) {
2208 - // ConPTY PseudoTerminal
2209 - // this.httprequest._term = require('win-virtual-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](80, 25);
2210 -
2211 - // The above line is commented out, because there is a bug with ClosePseudoConsole() API, so this is the workaround
2212 - 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] } });
2213 - this.httprequest._dispatcher.httprequest = this.httprequest;
2214 - this.httprequest._dispatcher.on('connection', function (c) { if (this.httprequest.connectionPromise.completed) { c.end(); } else { this.httprequest.connectionPromise._res(c); } });
2215 - }
2216 - else {
2217 - // Legacy Terminal
2218 - this.httprequest.connectionPromise._res(require('win-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](cols, rows));
2219 - }
2220 - }
2221 - else {
2222 - // Logged in user
2223 - var userPromise = require('user-sessions').enumerateUsers();
2224 - userPromise.that = this;
2225 - userPromise.then(function (u) {
2226 - var that = this.that;
2227 - if (u.Active.length > 0) {
2228 - var username = '"' + u.Active[0].Domain + '\\' + u.Active[0].Username + '"';
2229 - //sendConsoleText('Terminal: ' + username);
2230 - if (require('win-virtual-terminal').supported) {
2231 - // ConPTY PseudoTerminal
2232 - 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] } });
2233 - }
2234 - else {
2235 - // Legacy Terminal
2236 - 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] } });
2237 - }
2238 - that.httprequest._dispatcher.ws = that;
2239 - that.httprequest._dispatcher.on('connection', function (c) { if (this.ws.httprequest.connectionPromise.completed) { c.end(); } else { this.ws.httprequest.connectionPromise._res(c); } });
2240 - }
2241 - });
2242 - }
2243 - } catch (ex) {
2244 - this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString());
2245 - }
2246 - }
2247 - else {
2248 - try {
2249 - var bash = fs.existsSync('/bin/bash') ? '/bin/bash' : false;
2250 - var sh = fs.existsSync('/bin/sh') ? '/bin/sh' : false;
2251 - var login = process.platform == 'linux' ? '/bin/login' : '/usr/bin/login';
2252 -
2253 - var env = { HISTCONTROL: 'ignoreboth' };
2254 - if (process.env['LANG']) { env['LANG'] = process.env['LANG']; }
2255 - if (process.env['PATH']) { env['PATH'] = process.env['PATH']; }
2256 - if (this.httprequest.xoptions) {
2257 - if (this.httprequest.xoptions.rows) { env.LINES = ('' + this.httprequest.xoptions.rows); }
2258 - if (this.httprequest.xoptions.cols) { env.COLUMNS = ('' + this.httprequest.xoptions.cols); }
2259 - }
2260 - var options = { type: childProcess.SpawnTypes.TERM, uid: (this.httprequest.protocol == 8) ? require('user-sessions').consoleUid() : null, env: env };
2261 - if (this.httprequest.xoptions && this.httprequest.xoptions.requireLogin) {
2262 - if (!require('fs').existsSync(login)) { throw ('Unable to spawn login process'); }
2263 - this.httprequest.connectionPromise._res(childProcess.execFile(login, ['login'], options)); // Start login shell
2264 - }
2265 - else if (bash) {
2266 - var p = childProcess.execFile(bash, ['bash'], options); // Start bash
2267 - // Spaces at the beginning of lines are needed to hide commands from the command history
2268 - if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string')) {
2269 - if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); }
2270 - } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); }
2271 - this.httprequest.connectionPromise._res(p);
2272 - }
2273 - else if (sh) {
2274 - var p = childProcess.execFile(sh, ['sh'], options); // Start sh
2275 - // Spaces at the beginning of lines are needed to hide commands from the command history
2276 - if ((obj.serverInfo.termlaunchcommand != null) && (typeof obj.serverInfo.termlaunchcommand[process.platform] == 'string')) {
2277 - if (obj.serverInfo.termlaunchcommand[process.platform] != '') { p.stdin.write(obj.serverInfo.termlaunchcommand[process.platform]); }
2278 - } else if (process.platform == 'linux') { p.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); }
2279 - this.httprequest.connectionPromise._res(p);
2280 - }
2281 - else {
2282 - this.httprequest.connectionPromise._rej('Failed to start remote terminal session, no shell found');
2283 - }
2284 - } catch (ex) {
2285 - this.httprequest.connectionPromise._rej('Failed to start remote terminal session, ' + ex.toString());
2286 - }
2287 - }
2288 -
2289 - this.httprequest.connectionPromise.then(
2290 - function (term) {
2291 - // SUCCESS
2292 - var stdoutstream;
2293 - var stdinstream;
2294 - if (process.platform == 'win32') {
2295 - this.ws.httprequest._term = term;
2296 - this.ws.httprequest._term.tunnel = this.ws;
2297 - stdoutstream = stdinstream = term;
2298 - }
2299 - else {
2300 - term.descriptorMetadata = 'Remote Terminal';
2301 - this.ws.httprequest.process = term;
2302 - this.ws.httprequest.process.tunnel = this.ws;
2303 - term.stderr.stdout = term.stdout;
2304 - term.stderr.on('data', function (c) { this.stdout.write(c); });
2305 - stdoutstream = term.stdout;
2306 - stdinstream = term.stdin;
2307 - this.ws.prependListener('end', function () { this.httprequest.process.kill(); });
2308 - term.prependListener('exit', function () { this.tunnel.end(); });
2309 - }
2310 -
2311 - this.ws.removeAllListeners('data');
2312 - this.ws.on('data', onTunnelControlData);
2313 -
2314 - stdoutstream.pipe(this.ws, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text.
2315 - this.ws.pipe(stdinstream, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text.
2316 -
2317 - // Add the terminal session to the count to update the server
2318 - if (this.ws.httprequest.userid != null) {
2319 - var userid = getUserIdAndGuestNameFromHttpRequest(this.ws.httprequest);
2320 - if (tunnelUserCount.terminal[userid] == null) { tunnelUserCount.terminal[userid] = 1; } else { tunnelUserCount.terminal[userid]++; }
2321 - try { mesh.SendCommand({ action: 'sessions', type: 'terminal', value: tunnelUserCount.terminal }); } catch (ex) { }
2322 - broadcastSessionsToRegisteredApps();
2323 - }
2324 -
2325 - // Toast Notification, if required
2326 - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 2)) {
2327 - // User Notifications is required
2328 - var notifyMessage = currentTranslation['terminalNotify'].replace('{0}', this.ws.httprequest.username);
2329 - var notifyTitle = "MeshCentral";
2330 - if (this.ws.httprequest.soptions != null) {
2331 - if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; }
2332 - 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); }
2333 - }
2334 - try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { }
2335 - }
2336 - },
2337 - function (e) {
2338 - // FAILED to connect terminal
2339 - this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2340 - this.ws.end();
2341 - });
2342 - },
2343 - function (e) {
2344 - // DO NOT start terminal
2345 - this.that.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2346 - this.that.end();
2347 - });
2736 + this.httprequest.tpromise.then(terminal_promise_consent_resolved, terminal_promise_consent_rejected);
2737 }
2349 - else if (this.httprequest.protocol == 2) {
2738 + else if (this.httprequest.protocol == 2)
2739 + {
2740 //
2741 // Remote Desktop
2742 //
@@ -2378,14 +2768,17 @@ function onTunnelData(data) {
2768
2769 // Send a metadata update to all desktop sessions
2770 var users = {};
2381 - if (this.httprequest.desktop.kvm.tunnels != null) {
2382 - for (var i in this.httprequest.desktop.kvm.tunnels) {
2771 + if (this.httprequest.desktop.kvm.tunnels != null)
2772 + {
2773 + for (var i in this.httprequest.desktop.kvm.tunnels)
2774 + {
2775 try {
2776 var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest.desktop.kvm.tunnels[i].httprequest);
2777 if (users[userid] == null) { users[userid] = 1; } else { users[userid]++; }
2778 } catch (ex) { sendConsoleText(ex); }
2779 }
2388 - for (var i in this.httprequest.desktop.kvm.tunnels) {
2780 + for (var i in this.httprequest.desktop.kvm.tunnels)
2781 + {
2782 try { this.httprequest.desktop.kvm.tunnels[i].write(JSON.stringify({ ctrlChannel: '102938', type: 'metadata', users: users })); } catch (ex) { }
2783 }
2784 tunnelUserCount.desktop = users;
@@ -2393,79 +2786,8 @@ function onTunnelData(data) {
2786 broadcastSessionsToRegisteredApps();
2787 }
2788
2396 - this.end = function () {
2397 - --this.desktop.kvm.connectionCount;
2398 -
2399 - // Remove ourself from the list of remote desktop session
2400 - var i = this.desktop.kvm.tunnels.indexOf(this);
2401 - if (i >= 0) { this.desktop.kvm.tunnels.splice(i, 1); }
2402 -
2403 - // Send a metadata update to all desktop sessions
2404 - var users = {};
2405 - if (this.httprequest.desktop.kvm.tunnels != null) {
2406 - for (var i in this.httprequest.desktop.kvm.tunnels) {
2407 - try {
2408 - var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest.desktop.kvm.tunnels[i].httprequest);
2409 - if (users[userid] == null) { users[userid] = 1; } else { users[userid]++; }
2410 - } catch (ex) { sendConsoleText(ex); }
2411 - }
2412 - for (var i in this.httprequest.desktop.kvm.tunnels) {
2413 - try { this.httprequest.desktop.kvm.tunnels[i].write(JSON.stringify({ ctrlChannel: '102938', type: 'metadata', users: users })); } catch (ex) { }
2414 - }
2415 - tunnelUserCount.desktop = users;
2416 - try { mesh.SendCommand({ action: 'sessions', type: 'kvm', value: users }); } catch (ex) { }
2417 - broadcastSessionsToRegisteredApps();
2418 - }
2789 + this.end = tunnel_kvm_end;
2790
2420 - // Unpipe the web socket
2421 - try {
2422 - this.unpipe(this.httprequest.desktop.kvm);
2423 - this.httprequest.desktop.kvm.unpipe(this);
2424 - } catch (ex) { }
2425 -
2426 - // Unpipe the WebRTC channel if needed (This will also be done when the WebRTC channel ends).
2427 - if (this.rtcchannel) {
2428 - try {
2429 - this.rtcchannel.unpipe(this.httprequest.desktop.kvm);
2430 - this.httprequest.desktop.kvm.unpipe(this.rtcchannel);
2431 - }
2432 - catch (ex) { }
2433 - }
2434 -
2435 - // Place wallpaper back if needed
2436 - // TODO
2437 -
2438 - if (this.desktop.kvm.connectionCount == 0) {
2439 - // Display a toast message. This may not be supported on all platforms.
2440 - // try { require('toaster').Toast('MeshCentral', 'Remote Desktop Control Ended.'); } catch (ex) { }
2441 -
2442 - this.httprequest.desktop.kvm.end();
2443 - if (this.httprequest.desktop.kvm.connectionBar) {
2444 - this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2445 - this.httprequest.desktop.kvm.connectionBar.close();
2446 - this.httprequest.desktop.kvm.connectionBar = null;
2447 - }
2448 - } else {
2449 - for (var i in this.httprequest.desktop.kvm.users) {
2450 - if ((this.httprequest.desktop.kvm.users[i] == this.httprequest.username) && this.httprequest.desktop.kvm.connectionBar) {
2451 - 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; } }
2452 - this.httprequest.desktop.kvm.users.splice(i, 1);
2453 - this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2454 - this.httprequest.desktop.kvm.connectionBar.close();
2455 - 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);
2456 - this.httprequest.desktop.kvm.connectionBar.httprequest = this.httprequest;
2457 - this.httprequest.desktop.kvm.connectionBar.on('close', function () {
2458 - MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest);
2459 - for (var i in this.httprequest.desktop.kvm._pipedStreams) {
2460 - this.httprequest.desktop.kvm._pipedStreams[i].end();
2461 - }
2462 - this.httprequest.desktop.kvm.end();
2463 - });
2464 - break;
2465 - }
2466 - }
2467 - }
2468 - };
2791 if (this.httprequest.desktop.kvm.hasOwnProperty('connectionCount')) {
2792 this.httprequest.desktop.kvm.connectionCount++;
2793 this.httprequest.desktop.kvm.rusers.push(this.httprequest.realname);
@@ -2478,31 +2800,38 @@ function onTunnelData(data) {
2800 this.httprequest.desktop.kvm.users = [this.httprequest.username];
2801 }
2802
2481 - if ((this.httprequest.desktopviewonly != true) && ((this.httprequest.rights == 0xFFFFFFFF) || (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) != 0) && ((this.httprequest.rights & MESHRIGHT_REMOTEVIEW) == 0)))) {
2803 + if ((this.httprequest.desktopviewonly != true) && ((this.httprequest.rights == 0xFFFFFFFF) || (((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) != 0) && ((this.httprequest.rights & MESHRIGHT_REMOTEVIEW) == 0))))
2804 + {
2805 // If we have remote control rights, pipe the KVM input
2806 this.pipe(this.httprequest.desktop.kvm, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text. Pipe the Browser --> KVM input.
2484 - } else {
2807 + }
2808 + else
2809 + {
2810 // We need to only pipe non-mouse & non-keyboard inputs.
2811 // sendConsoleText('Warning: No Remote Desktop Input Rights.');
2812 // TODO!!!
2813 }
2814
2815 // Perform notification if needed. Toast messages may not be supported on all platforms.
2491 - if (this.httprequest.consent && (this.httprequest.consent & 8)) {
2816 + if (this.httprequest.consent && (this.httprequest.consent & 8))
2817 + {
2818 // User Consent Prompt is required
2819 // Send a console message back using the console channel, "\n" is supported.
2820 this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
2821 var consentMessage = currentTranslation['desktopConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username);
2822 var consentTitle = 'MeshCentral';
2497 - if (this.httprequest.soptions != null) {
2823 + if (this.httprequest.soptions != null)
2824 + {
2825 if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; }
2826 if (this.httprequest.soptions.consentMsgDesktop != null) { consentMessage = this.httprequest.soptions.consentMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
2827 }
2828 var pr;
2502 - if (process.platform == 'win32') {
2829 + if (process.platform == 'win32')
2830 + {
2831 var enhanced = false;
2832 try { require('win-userconsent'); enhanced = true; } catch (ex) { }
2505 - if (enhanced) {
2833 + if (enhanced)
2834 + {
2835 var ipr = server_getUserImage(this.httprequest.userid);
2836 ipr.consentTitle = consentTitle;
2837 ipr.consentMessage = consentMessage;
@@ -2511,85 +2840,33 @@ function onTunnelData(data) {
2840 ipr.tsid = tsid;
2841 ipr.username = this.httprequest.realname;
2842 ipr.translation = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
2514 - pr = ipr.then(function (img) {
2843 + pr = ipr.then(function (img)
2844 + {
2845 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 });
2846 this.__childPromise.close = this.consent.close.bind(this.consent);
2847 return (this.consent);
2848 });
2849 }
2520 - else {
2521 - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null, tsid);
2850 + else
2851 + {
2852 + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null, tsid);
2853 }
2854 }
2524 - else {
2525 - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null, tsid);
2855 + else
2856 + {
2857 + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null, tsid);
2858 }
2859 pr.ws = this;
2860 this.pause();
2861 this._consentpromise = pr;
2530 - this.prependOnceListener('end', function () {
2531 - if (this._consentpromise && this._consentpromise.close) {
2532 - this._consentpromise.close();
2533 - }
2534 - });
2535 - pr.then(
2536 - function (always) {
2537 - if (always) { server_set_consentTimer(this.ws.httprequest.userid); }
2538 -
2539 - // Success
2540 - this.ws._consentpromise = null;
2541 - MeshServerLogEx(30, null, "Starting remote desktop after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2542 - this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null, msgid: 0 }));
2543 - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 1)) {
2544 - // User Notifications is required
2545 - var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.ws.httprequest.realname);
2546 - var notifyTitle = "MeshCentral";
2547 - if (this.ws.httprequest.soptions != null) {
2548 - if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; }
2549 - 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); }
2550 - }
2551 - try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { }
2552 - }
2553 - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 0x40)) {
2554 - // Connection Bar is required
2555 - if (this.ws.httprequest.desktop.kvm.connectionBar) {
2556 - this.ws.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2557 - this.ws.httprequest.desktop.kvm.connectionBar.close();
2558 - }
2559 - try {
2560 - 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);
2561 - MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2562 - } catch (ex) {
2563 - if (process.platform != 'darwin') {
2564 - MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or Not Supported (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2565 - }
2566 - }
2567 - if (this.ws.httprequest.desktop.kvm.connectionBar) {
2568 - this.ws.httprequest.desktop.kvm.connectionBar.httprequest = this.ws.httprequest;
2569 - this.ws.httprequest.desktop.kvm.connectionBar.on('close', function () {
2570 - MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest);
2571 - for (var i in this.httprequest.desktop.kvm._pipedStreams) {
2572 - this.httprequest.desktop.kvm._pipedStreams[i].end();
2573 - }
2574 - this.httprequest.desktop.kvm.end();
2575 - });
2576 - }
2577 - }
2578 - this.ws.httprequest.desktop.kvm.pipe(this.ws, { dataTypeSkip: 1 });
2579 - if (this.ws.httprequest.autolock) {
2580 - destopLockHelper_pipe(this.ws.httprequest);
2581 - }
2582 - this.ws.resume();
2583 - },
2584 - function (e) {
2585 - // User Consent Denied/Failed
2586 - this.ws._consentpromise = null;
2587 - MeshServerLogEx(34, null, "Failed to start remote desktop after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2588 - this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2589 - });
2590 - } else {
2862 + this.prependOnceListener('end', kvm_tunnel_consentpromise_closehandler);
2863 + pr.then(kvm_consentpromise_resolved, kvm_consentpromise_rejected);
2864 + }
2865 + else
2866 + {
2867 // User Consent Prompt is not required
2592 - if (this.httprequest.consent && (this.httprequest.consent & 1)) {
2868 + if (this.httprequest.consent && (this.httprequest.consent & 1))
2869 + {
2870 // User Notifications is required
2871 MeshServerLogEx(35, null, "Started remote desktop with toast notification (" + this.httprequest.remoteaddr + ")", this.httprequest);
2872 var notifyMessage = currentTranslation['desktopNotify'].replace('{0}', this.httprequest.realname);
@@ -2599,34 +2876,52 @@ function onTunnelData(data) {
2876 if (this.httprequest.soptions.notifyMsgDesktop != null) { notifyMessage = this.httprequest.soptions.notifyMsgDesktop.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
2877 }
2878 try { require('toaster').Toast(notifyTitle, notifyMessage, tsid); } catch (ex) { }
2602 - } else {
2879 + } else
2880 + {
2881 MeshServerLogEx(36, null, "Started remote desktop without notification (" + this.httprequest.remoteaddr + ")", this.httprequest);
2882 }
2605 - if (this.httprequest.consent && (this.httprequest.consent & 0x40)) {
2883 + if (this.httprequest.consent && (this.httprequest.consent & 0x40))
2884 + {
2885 // Connection Bar is required
2607 - if (this.httprequest.desktop.kvm.connectionBar) {
2886 + if (this.httprequest.desktop.kvm.connectionBar)
2887 + {
2888 this.httprequest.desktop.kvm.connectionBar.removeAllListeners('close');
2889 this.httprequest.desktop.kvm.connectionBar.close();
2890 }
2611 - try {
2891 + try
2892 + {
2893 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);
2894 MeshServerLogEx(31, null, "Remote Desktop Connection Bar Activated/Updated (" + this.httprequest.remoteaddr + ")", this.httprequest);
2895 } catch (ex) {
2896 MeshServerLogEx(32, null, "Remote Desktop Connection Bar Failed or not Supported (" + this.httprequest.remoteaddr + ")", this.httprequest);
2897 }
2617 - if (this.httprequest.desktop.kvm.connectionBar) {
2618 - this.httprequest.desktop.kvm.connectionBar.httprequest = this.httprequest;
2619 - this.httprequest.desktop.kvm.connectionBar.on('close', function () {
2620 - MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.httprequest.remoteaddr + ")", this.httprequest);
2621 - for (var i in this.httprequest.desktop.kvm._pipedStreams) {
2622 - this.httprequest.desktop.kvm._pipedStreams[i].end();
2898 + if (this.httprequest.desktop.kvm.connectionBar)
2899 + {
2900 + this.httprequest.desktop.kvm.connectionBar.state =
2901 + {
2902 + userid: this.httprequest.userid,
2903 + xuserid: this.httprequest.xuserid,
2904 + username: this.httprequest.username,
2905 + sessionid: this.httprequest.sessionid,
2906 + remoteaddr: this.httprequest.remoteaddr,
2907 + guestname: this.httprequest.guestname,
2908 + desktop: this.httprequest.desktop
2909 + };
2910 + this.httprequest.desktop.kvm.connectionBar.on('close', function ()
2911 + {
2912 + console.info1('Connection Bar Forcefully closed');
2913 + MeshServerLogEx(29, null, "Remote Desktop Connection forcefully closed by local user (" + this.state.remoteaddr + ")", this.state);
2914 + for (var i in this.state.desktop.kvm._pipedStreams)
2915 + {
2916 + this.state.desktop.kvm._pipedStreams[i].end();
2917 }
2624 - this.httprequest.desktop.kvm.end();
2918 + this.state.desktop.kvm.end();
2919 });
2920 }
2921 }
2922 this.httprequest.desktop.kvm.pipe(this, { dataTypeSkip: 1 });
2629 - if (this.httprequest.autolock) {
2923 + if (this.httprequest.autolock)
2924 + {
2925 destopLockHelper_pipe(this.httprequest);
2926 }
2927 }
@@ -2634,7 +2929,6 @@ function onTunnelData(data) {
2929 this.removeAllListeners('data');
2930 this.on('data', onTunnelControlData);
2931 //this.write('MeshCore KVM Hello!1');
2637 -
2932 } else if (this.httprequest.protocol == 5) {
2933 //
2934 // Remote Files
@@ -2659,7 +2953,8 @@ function onTunnelData(data) {
2953 broadcastSessionsToRegisteredApps();
2954 }
2955
2662 - this.end = function () {
2956 + this.end = function ()
2957 + {
2958 // Remove the files session from the count to update the server
2959 if (this.httprequest.userid != null) {
2960 var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest);
@@ -2670,22 +2965,26 @@ function onTunnelData(data) {
2965 };
2966
2967 // Perform notification if needed. Toast messages may not be supported on all platforms.
2673 - if (this.httprequest.consent && (this.httprequest.consent & 32)) {
2968 + if (this.httprequest.consent && (this.httprequest.consent & 32))
2969 + {
2970 // User Consent Prompt is required
2971 // Send a console message back using the console channel, "\n" is supported.
2972 this.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 }));
2973 var consentMessage = currentTranslation['fileConsent'].replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username);
2974 var consentTitle = 'MeshCentral';
2975
2680 - if (this.httprequest.soptions != null) {
2976 + if (this.httprequest.soptions != null)
2977 + {
2978 if (this.httprequest.soptions.consentTitle != null) { consentTitle = this.httprequest.soptions.consentTitle; }
2979 if (this.httprequest.soptions.consentMsgFiles != null) { consentMessage = this.httprequest.soptions.consentMsgFiles.replace('{0}', this.httprequest.realname).replace('{1}', this.httprequest.username); }
2980 }
2981 var pr;
2685 - if (process.platform == 'win32') {
2982 + if (process.platform == 'win32')
2983 + {
2984 var enhanced = false;
2985 try { require('win-userconsent'); enhanced = true; } catch (ex) { }
2688 - if (enhanced) {
2986 + if (enhanced)
2987 + {
2988 var ipr = server_getUserImage(this.httprequest.userid);
2989 ipr.consentTitle = consentTitle;
2990 ipr.consentMessage = consentMessage;
@@ -2693,49 +2992,29 @@ function onTunnelData(data) {
2992 ipr.consentAutoAccept = this.httprequest.consentAutoAccept;
2993 ipr.username = this.httprequest.realname;
2994 ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage };
2696 - pr = ipr.then(function (img) {
2995 + pr = ipr.then(function (img)
2996 + {
2997 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 });
2998 this.__childPromise.close = this.consent.close.bind(this.consent);
2999 return (this.consent);
3000 });
2701 - } else {
2702 - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null);
3001 + } else
3002 + {
3003 + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null);
3004 }
2704 - } else {
2705 - pr = require('message-box').create(consentTitle, consentMessage, this.consentTimeout, null);
3005 + }
3006 + else
3007 + {
3008 + pr = require('message-box').create(consentTitle, consentMessage, this.httprequest.consentTimeout, null);
3009 }
3010 pr.ws = this;
3011 this.pause();
3012 this._consentpromise = pr;
2710 - this.prependOnceListener('end', function () { if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); } });
2711 - pr.then(
2712 - function (always) {
2713 - if (always) { server_set_consentTimer(this.ws.httprequest.userid); }
2714 -
2715 - // Success
2716 - this.ws._consentpromise = null;
2717 - MeshServerLogEx(40, null, "Starting remote files after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2718 - this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null }));
2719 - if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 4)) {
2720 - // User Notifications is required
2721 - var notifyMessage = currentTranslation['fileNotify'].replace('{0}', this.ws.httprequest.realname);
2722 - var notifyTitle = "MeshCentral";
2723 - if (this.ws.httprequest.soptions != null) {
2724 - if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; }
2725 - 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); }
2726 - }
2727 - try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { }
2728 - }
2729 - this.ws.resume();
2730 - },
2731 - function (e) {
2732 - // User Consent Denied/Failed
2733 - this.ws._consentpromise = null;
2734 - MeshServerLogEx(41, null, "Failed to start remote files after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest);
2735 - this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 }));
2736 - });
3013 + this.prependOnceListener('end', files_tunnel_endhandler);
3014 + pr.then(files_consentpromise_resolved, files_consentpromise_rejected);
3015 }
2738 - else {
3016 + else
3017 + {
3018 // User Consent Prompt is not required
3019 if (this.httprequest.consent && (this.httprequest.consent & 4)) {
3020 // User Notifications is required
@@ -3062,6 +3341,44 @@ function onTunnelWebRTCControlData(data) {
3341 }
3342 }
3343
3344 +function tunnel_webrtc_onEnd()
3345 +{
3346 + // The WebRTC channel closed, unpipe the KVM now. This is also done when the web socket closes.
3347 + //sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC data channel closed');
3348 + if (this.websocket.desktop && this.websocket.desktop.kvm)
3349 + {
3350 + try
3351 + {
3352 + this.unpipe(this.websocket.desktop.kvm);
3353 + this.websocket.httprequest.desktop.kvm.unpipe(this);
3354 + } catch (ex) { }
3355 + }
3356 + this.httprequest = null;
3357 + this.websocket = null;
3358 +}
3359 +function tunnel_webrtc_DataChannel_OnFinalized()
3360 +{
3361 + console.info1('WebRTC DataChannel Finalized');
3362 +}
3363 +function tunnel_webrtc_OnDataChannel(rtcchannel)
3364 +{
3365 + //sendConsoleText('WebRTC Datachannel open, protocol: ' + this.websocket.httprequest.protocol);
3366 + //rtcchannel.maxFragmentSize = 32768;
3367 + rtcchannel.xrtc = this;
3368 + rtcchannel.websocket = this.websocket;
3369 + this.rtcchannel = rtcchannel;
3370 + this.rtcchannel.once('~', tunnel_webrtc_DataChannel_OnFinalized);
3371 + this.websocket.rtcchannel = rtcchannel;
3372 + this.websocket.rtcchannel.on('data', onTunnelWebRTCControlData);
3373 + this.websocket.rtcchannel.on('end', tunnel_webrtc_onEnd);
3374 + this.websocket.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc0\"}'); // Indicate we are ready for WebRTC switch-over.
3375 +}
3376 +
3377 +function tunnel_webrtc_OnFinalized()
3378 +{
3379 + console.info1('WebRTC Connection Finalized');
3380 +}
3381 +
3382 // Called when receiving control data on websocket
3383 function onTunnelControlData(data, ws) {
3384 var obj;
@@ -3135,7 +3452,8 @@ function onTunnelControlData(data, ws) {
3452 break;
3453 }
3454 case 'webrtc0': { // Browser indicates we can start WebRTC switch-over.
3138 - if (ws.httprequest.protocol == 1) { // Terminal
3455 + if (ws.httprequest.protocol == 1)
3456 + { // Terminal
3457 // 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
3458 if (process.platform == 'win32') {
3459 ws.httprequest._term.unpipe(ws);
@@ -3146,7 +3464,8 @@ function onTunnelControlData(data, ws) {
3464 } else if (ws.httprequest.protocol == 2) { // Desktop
3465 // 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
3466 ws.httprequest.desktop.kvm.unpipe(ws);
3149 - } else {
3467 + } else
3468 + {
3469 // Switch things around so all WebRTC data goes to onTunnelData().
3470 ws.rtcchannel.httprequest = ws.httprequest;
3471 ws.rtcchannel.removeAllListeners('data');
@@ -3155,8 +3474,10 @@ function onTunnelControlData(data, ws) {
3474 ws.write("{\"ctrlChannel\":\"102938\",\"type\":\"webrtc1\"}"); // End of data marker
3475 break;
3476 }
3158 - case 'webrtc1': {
3159 - if ((ws.httprequest.protocol == 1) || (ws.httprequest.protocol == 6)) { // Terminal
3477 + case 'webrtc1':
3478 + {
3479 + if ((ws.httprequest.protocol == 1) || (ws.httprequest.protocol == 6))
3480 + { // Terminal
3481 // Switch the user input from websocket to webrtc at this point.
3482 if (process.platform == 'win32') {
3483 ws.unpipe(ws.httprequest._term);
@@ -3166,7 +3487,9 @@ function onTunnelControlData(data, ws) {
3487 ws.rtcchannel.pipe(ws.httprequest.process.stdin, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text.
3488 }
3489 ws.resume(); // Resume the websocket to keep receiving control data
3169 - } else if (ws.httprequest.protocol == 2) { // Desktop
3490 + }
3491 + else if (ws.httprequest.protocol == 2)
3492 + { // Desktop
3493 // Switch the user input from websocket to webrtc at this point.
3494 ws.unpipe(ws.httprequest.desktop.kvm);
3495 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) {
3516 // This is a WebRTC offer.
3517 if ((ws.httprequest.protocol == 1) || (ws.httprequest.protocol == 6)) return; // TODO: Terminal is currently broken with WebRTC. Reject WebRTC upgrade for now.
3518 ws.webrtc = rtc.createConnection();
3519 + ws.webrtc.once('~', tunnel_webrtc_OnFinalized);
3520 ws.webrtc.websocket = ws;
3197 - ws.webrtc.on('connected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC connected');*/ });
3198 - ws.webrtc.on('disconnected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC disconnected');*/ });
3199 - ws.webrtc.on('dataChannel', function (rtcchannel) {
3200 - //sendConsoleText('WebRTC Datachannel open, protocol: ' + this.websocket.httprequest.protocol);
3201 - //rtcchannel.maxFragmentSize = 32768;
3202 - rtcchannel.xrtc = this;
3203 - rtcchannel.websocket = this.websocket;
3204 - this.rtcchannel = rtcchannel;
3205 - this.websocket.rtcchannel = rtcchannel;
3206 - this.websocket.rtcchannel.on('data', onTunnelWebRTCControlData);
3207 - this.websocket.rtcchannel.on('end', function () {
3208 - // The WebRTC channel closed, unpipe the KVM now. This is also done when the web socket closes.
3209 - //sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC data channel closed');
3210 - if (this.websocket.desktop && this.websocket.desktop.kvm) {
3211 - try {
3212 - this.unpipe(this.websocket.desktop.kvm);
3213 - this.websocket.httprequest.desktop.kvm.unpipe(this);
3214 - } catch (ex) { }
3215 - }
3216 - });
3217 - this.websocket.write('{\"ctrlChannel\":\"102938\",\"type\":\"webrtc0\"}'); // Indicate we are ready for WebRTC switch-over.
3218 - });
3521 + //ws.webrtc.on('connected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC connected');*/ });
3522 + //ws.webrtc.on('disconnected', function () { /*sendConsoleText('Tunnel #' + this.websocket.tunnel.index + ' WebRTC disconnected');*/ });
3523 + ws.webrtc.on('dataChannel', tunnel_webrtc_OnDataChannel);
3524 +
3525 var sdp = null;
3526 try { sdp = ws.webrtc.setOffer(obj.sdp); } catch (ex) { }
3527 if (sdp != null) { ws.write({ type: 'answer', ctrlChannel: '102938', sdp: sdp }); }
@@ -4040,7 +4346,9 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
4346 this._dispatcher.on('connection', function (c) {
4347 this._c = c;
4348 this._c.root = this.parent;
4043 - this._c.on('end', function () {
4349 + this._c.on('end', function ()
4350 + {
4351 + this.root._dispatcher.close();
4352 this.root._dispatcher = null;
4353 this.root = null;
4354 });
apprelays.js
+8 -1
@@ -69,7 +69,7 @@ function SerialTunnel(options) {
69 }
70
71 // Construct a Web relay object
72 -module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid, sessionid) {
72 +module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid, sessionid, expire) {
73 const obj = {};
74 obj.parent = parent;
75 obj.lastOperation = Date.now();
@@ -80,6 +80,7 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
80 obj.port = port;
81 obj.appid = appid;
82 obj.sessionid = sessionid;
83 + obj.expireTimer = null;
84 var pendingRequests = [];
85 var nextTunnelId = 1;
86 var tunnels = {};
@@ -90,6 +91,9 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
91 // Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
92 obj.webCookies = {};
93
94 + // Setup an expire time if needed
95 + if (expire != null) { var timeout = (expire - Date.now()); if (timeout < 10) { timeout = 10; } obj.expireTimer = setTimeout(close, timeout); }
96 +
97 // Events
98 obj.closed = false;
99 obj.onclose = null;
@@ -202,6 +206,9 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
206 parent.parent.debug('webrelay', 'tunnel-close');
207 obj.closed = true;
208
209 + // Clear the time if present
210 + if (obj.expireTimer != null) { clearTimeout(obj.expireTimer); delete obj.expireTimer; }
211 +
212 // Close all tunnels
213 for (var i in tunnels) { tunnels[i].close(); }
214 tunnels = null;
meshcentral.js
+1 -1
@@ -466,7 +466,7 @@ function CreateMeshCentralServer(config, args) {
466 const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
467 const env = Object.assign({}, process.env); // Shallow clone
468 if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
469 - const xxprocess = child_process.exec(npmpath + ' install meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
469 + const xxprocess = child_process.exec(npmpath + ' install --no-package-lock meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
470 if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
471 });
472 xxprocess.data = '';
meshuser.js
+3 -2
@@ -4137,8 +4137,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
4137 else if ((command.start != null) && (typeof command.start != 'number')) { err = 'Invalid start time'; } // Check the start time in UTC seconds
4138 else if ((command.end != null) && (typeof command.end != 'number')) { err = 'Invalid end time'; } // Check the end time in UTC seconds
4139 else if (common.validateInt(command.consent, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
4140 - else if (common.validateInt(command.p, 1, 7) == false) { err = 'Invalid protocol'; } // Check the protocol, 1 = Terminal, 2 = Desktop, 4 = Files
4140 + 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
4141 else if ((command.recurring != null) && (common.validateInt(command.recurring, 1, 2) == false)) { err = 'Invalid recurring value'; } // Check the recurring value, 1 = Daily, 2 = Weekly
4142 + else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present
4143 else if ((command.recurring != null) && ((command.end != null) || (command.start == null) || (command.expire == null))) { err = 'Invalid recurring command'; }
4144 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
4145 else {
@@ -4238,7 +4239,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
4239 try { ws.send(JSON.stringify(command)); } catch (ex) { }
4240
4241 // Create a device sharing database entry
4241 - 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 };
4242 + 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 };
4243 if ((startTime != null) && (expireTime != null)) { shareEntry.startTime = startTime; shareEntry.expireTime = expireTime; }
4244 else if ((startTime != null) && (duration != null)) { shareEntry.startTime = startTime; shareEntry.duration = duration; }
4245 if (command.recurring) { shareEntry.recurring = command.recurring; }
views/default.handlebars
+20 -5
@@ -3650,7 +3650,7 @@
3650 if (message.consent & 0x0040) { y.push("Privacy bar"); }
3651 if (y.length == 0) { y.push("None"); }
3652 x += addHtmlValue("User Consent", y.join(', '));
3653 - 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];
3653 + 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); }
3654 x += '<div id=agentInvitationLinkDiv style="text-align:center;font-size:large;margin:16px"><a href="' + message.url + '" id=agentInvitationLink rel="noreferrer noopener" target="_blank" style=cursor:pointer>' + type + '</a> <img src=images/link4.png height=10 width=10 title="' + "Copy link to clipboard" + '" style=cursor:pointer onclick=d2CopyInviteToClip()></div></div>';
3655 setDialogMode(2, "Share Device", 1, null, x);
3656 break;
@@ -7588,7 +7588,7 @@
7588 var dshare = deviceShares[i], trash = '';
7589 if (dshare.url != null) { trash += '<a href="' + dshare.url + '" rel="noreferrer noopener" target=_blank title="' + "Device Sharing Link" + '" style=cursor:pointer><img src=images/link2.png border=0 height=10 width=10></a> '; }
7590 trash += '<a href=# onclick=\'return p30removeDeviceSharing(event,"' + encodeURIComponentEx(currentNode._id) + '","' + encodeURIComponentEx(dshare.publicid) + '","' + encodeURIComponentEx(dshare.guestName) + '")\' title="' + "Remove device sharing" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
7591 - var type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Files", "Terminal + Files", "Desktop + Files", "Desktop + Terminal + Files"][dshare.p];
7591 + 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; }
7592 var details = type;
7593 if ((dshare.startTime != null) && (dshare.expireTime != null)) { details = format("{0}, {1} to {2}", type, printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); }
7594 if ((dshare.startTime != null) && (dshare.duration != null)) {
@@ -7896,6 +7896,11 @@
7896 if ((rights != 0xFFFFFFFF) && ((rights & 0x600) != 0)) { termFiles = ''; }
7897 var allFeatures = '<option value=7>' + "Desktop + Terminal + Files" + '</option>';
7898 if ((rights != 0xFFFFFFFF) && ((rights & 0x700) != 0)) { allFeatures = ''; }
7899 + var httpFeature = '';
7900 + if (webRelayPort != 0) {
7901 + httpFeature = '<option value=8>' + "HTTP" + '</option><option value=9>' + "HTTPS" + '</option>';
7902 + if ((rights != 0xFFFFFFFF) && ((rights & 8) != 0)) { httpFeature = ''; }
7903 + }
7904
7905 var y = '', z = '';
7906 if ((currentNode.agent.caps & 1) == 1) { y += (deskFull + '<option value=3>' + "Desktop, View only" + '</option>'); } // Agent is desktop capable
@@ -7904,6 +7909,7 @@
7909 if ((currentNode.agent.caps & 5) == 5) { y += deskFiles; } // Agent is desktop + files capable
7910 if ((currentNode.agent.caps & 6) == 6) { y += termFiles; } // Agent is terminal + files capable
7911 if ((currentNode.agent.caps & 7) == 7) { y += allFeatures; } // Agent is desktop + terminal + files capable
7912 + y += httpFeature;
7913
7914 x += addHtmlValue("Type", '<select id=d2shareType style=float:right;width:250px onchange=showShareDeviceValidate()>' + y + '</select>');
7915 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 @@
7931 x += addHtmlValue("Start Time", '<input id=d2timeStartSelector style=float:right;width:250px class=flatpickr type="text" placeholder="' + "Select Date & Time..." + '" data-id="altinput">');
7932 x += addHtmlValue("Duration", '<select id=d2inviteDuration style=float:right;width:250px>' + z + '</select>');
7933 x += '</div>';
7928 - if (currentNode.agent.caps & 1) { x += addHtmlValue("User Consent", '<select id=d2userConsent style=float:right;width:250px><option value=1>' + "Prompt for consent" + '</option><option value=0>' + "Notify Only" + '</option></select>'); }
7934 + if (currentNode.agent.caps & 1) { x += '<div id=d2userConsentSelector>' + addHtmlValue("User Consent", '<select id=d2userConsent style=float:right;width:250px><option value=1>' + "Prompt for consent" + '</option><option value=0>' + "Notify Only" + '</option></select>') + '</div>'; }
7935 + x += '<div id=d2httpPortSelector>' + addHtmlValue("Port", '<input id=d2httpPort style=float:right;width:250px value=80 onkeyup=showShareDeviceValidate()></input>') + '</div>';
7936 + x += '<div id=d2httpsPortSelector>' + addHtmlValue("Port", '<input id=d2httpsPort style=float:right;width:250px value=443 onkeyup=showShareDeviceValidate()></input>') + '</div>';
7937 setDialogMode(2, "Share Device", 3, showShareDeviceEx, x);
7938 showShareDeviceValidate();
7939 var tomorrow = new Date();
@@ -7936,18 +7944,25 @@
7944 }
7945
7946 function showShareDeviceValidate() {
7947 + if (currentNode.agent.caps & 1) { QV('d2userConsentSelector', Q('d2shareType').value < 8); }
7948 + QV('d2httpPortSelector', Q('d2shareType').value == 8);
7949 + QV('d2httpsPortSelector', Q('d2shareType').value == 9);
7950 QV('d2modenow', Q('d2timeRange').value == 0);
7951 QV('d2moderange', Q('d2timeRange').value == 1);
7952 QV('d2moderecurring', Q('d2timeRange').value >= 2);
7953 var ok = true;
7954 + if (Q('d2shareType').value == 8) { var port = parseInt(Q('d2httpPort').value); if ((Q('d2httpPort').value != port) || (port < 1) || (port > 65535)) { ok = false; } }
7955 + if (Q('d2shareType').value == 9) { var port = parseInt(Q('d2httpsPort').value); if ((Q('d2httpsPort').value != port) || (port < 1) || (port > 65535)) { ok = false; } }
7956 if (Q('d2inviteName').value.trim().length == 0) { ok = false; }
7957 QE('idx_dlgOkButton', ok);
7958 }
7959
7960 function showShareDeviceEx(b, tag) {
7961 var consent = 0, p = parseInt(Q('d2shareType').value), viewOnly = false, q = 0;
7962 + 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; }
7963 + 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; }
7964 if (p == 3) { viewOnly = true; }
7950 - var q = [0, 1, 2, 2, 4, 6, 5, 7][p]; // Protocol flags: 1 = Terminal, 2 = Desktop, 4 = Files.
7965 + var q = [0, 1, 2, 2, 4, 6, 5, 7][p]; // Protocol flags: 1 = Terminal, 2 = Desktop, 4 = Files, 8 = HTTP, 16 = HTTPS.
7966
7967 if (q & 1) {
7968 consent |= 0x0002; // Terminal notify
@@ -12641,7 +12656,7 @@
12656 var dshare = deviceShares[i], trash = '';
12657 if (dshare.url != null) { trash += '<a href="' + dshare.url + '" rel="noreferrer noopener" target=_blank title="' + "Device Sharing Link" + '" style=cursor:pointer><img src=images/link2.png border=0 height=10 width=10></a> '; }
12658 trash += '<a href=# onclick=\'return p30removeDeviceSharing(event,"' + encodeURIComponentEx(dshare.nodeid) + '","' + encodeURIComponentEx(dshare.publicid) + '","' + encodeURIComponentEx(dshare.guestName) + '")\' title="' + "Remove device sharing" + '" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
12644 - var type = ['', "Terminal", "Desktop", "Desktop + Terminal", "Files", "Terminal + Files", "Desktop + Files", "Desktop + Terminal + Files"][dshare.p];
12659 + 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; }
12660 var details = type;
12661 if ((dshare.startTime != null) && (dshare.expireTime != null)) { details = format("{0}, {1} to {2}", type, printFlexDateTime(new Date(dshare.startTime)), printFlexDateTime(new Date(dshare.expireTime))); }
12662 if ((dshare.startTime != null) && (dshare.duration != null)) {
webrelayserver.js
+75 -39
@@ -124,8 +124,11 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
124 return next();
125 } else {
126 // If this is a normal request (GET, POST, etc) handle it here
127 - if ((req.session.userid != null) && (req.session.x != null) && (parent.webserver.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) {
128 - var relaySession = relaySessions[req.session.userid + '/' + req.session.x];
127 + var webSessionId = null;
128 + if ((req.session.userid != null) && (req.session.x != null)) { webSessionId = req.session.userid + '/' + req.session.x; }
129 + else if (req.session.z != null) { webSessionId = req.session.z; }
130 + if ((webSessionId != null) && (parent.webserver.destroyedSessions[webSessionId] == null)) {
131 + var relaySession = relaySessions[webSessionId];
132 if (relaySession != null) {
133 // The web relay session is valid, use it
134 relaySession.handleRequest(req, res);
@@ -157,8 +160,11 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
160
161 // Handle incoming web socket calls
162 obj.app.ws('/*', function (ws, req) {
160 - if ((req.session.userid != null) && (req.session.x != null) && (parent.webserver.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) {
161 - var relaySession = relaySessions[req.session.userid + '/' + req.session.x];
163 + var webSessionId = null;
164 + if ((req.session.userid != null) && (req.session.x != null)) { webSessionId = req.session.userid + '/' + req.session.x; }
165 + else if (req.session.z != null) { webSessionId = req.session.z; }
166 + if ((webSessionId != null) && (parent.webserver.destroyedSessions[webSessionId] == null)) {
167 + var relaySession = relaySessions[webSessionId];
168 if (relaySession != null) {
169 // The multi-tunnel session is valid, use it
170 relaySession.handleWebSocket(ws, req);
@@ -178,55 +184,85 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
184 parent.debug('webrelay', 'webRelaySetup');
185
186 // Decode the relay cookie
181 - if (req.query.c != null) {
182 - // Decode and check if this relay cookie is valid
183 - const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey);
184 - if ((urlCookie != null) && (urlCookie.ruserid != null) && (urlCookie.x != null) && (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] == null)) {
185 - if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
186 - if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
187 - }
188 - }
187 + if (req.query.c == null) { res.sendStatus(404); return; }
188 +
189 + // Decode and check if this relay cookie is valid
190 + var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire;
191 + const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey);
192 + if (urlCookie == null) { res.sendStatus(404); return; }
193
190 - // Check that all the required arguments are present
191 - 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; }
194 + // Decode the incomign cookie
195 + if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
196 + if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
197 +
198 + // This is a standard user, figure out what our web relay will be.
199 + if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
200 + if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
201 + if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
202 + userid = req.session.userid;
203 + domainid = userid.split('/')[1];
204 + domain = parent.config.domains[domainid];
205 + nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
206 + addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
207 + port = parseInt(req.query.p);
208 + appid = parseInt(req.query.appid);
209 + webSessionId = req.session.userid + '/' + req.session.x;
210 +
211 + // Check that all the required arguments are present
212 + 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; }
213 + } else if (urlCookie.r == 8) {
214 + // This is a guest user, figure out what our web relay will be.
215 + userid = urlCookie.userid;
216 + domainid = userid.split('/')[1];
217 + domain = parent.config.domains[domainid];
218 + nodeid = urlCookie.nid;
219 + addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
220 + port = urlCookie.port;
221 + appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
222 + webSessionId = userid + '/' + urlCookie.pid;
223 + if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
224 + if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
225 + if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
226 + expire = urlCookie.expire;
227 + }
228
193 - // Get the user and domain information
194 - const userid = req.session.userid;
195 - const domainid = userid.split('/')[1];
196 - const domain = parent.config.domains[domainid];
197 - const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
198 - const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
199 - const port = parseInt(req.query.p);
200 - const appid = parseInt(req.query.appid);
229 + // No session identifier was setup, exit now
230 + if (webSessionId == null) { res.sendStatus(404); return; }
231
232 // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
203 - const xrelaySession = relaySessions[req.session.userid + '/' + req.session.x];
233 + const xrelaySession = relaySessions[webSessionId];
234 if ((xrelaySession != null) && (xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
235 // We found an exact match, we are all setup already, redirect to root
236 res.redirect('/');
237 return;
238 }
239
210 - // There is a relay session, but it's not correct, close it.
211 - if (xrelaySession != null) { xrelaySession.close(); delete relaySessions[req.session.userid + '/' + req.session.x]; }
240 + // Check that the user has rights to access this device
241 + parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
242 + // If there is no remote control rights, reject this web relay
243 + if ((rights & 8) == 0) { res.sendStatus(404); return; }
244
213 - // Create a web relay session
214 - const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySession);
215 - relaySession.onclose = function (sessionId) {
216 - // Remove the relay session
217 - delete relaySessions[sessionId];
218 - // If there are not more relay sessions, clear the cleanup timer
219 - if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; }
220 - }
245 + // There is a relay session, but it's not correct, close it.
246 + if (xrelaySession != null) { xrelaySession.close(); delete relaySessions[webSessionId]; }
247 +
248 + // Create a web relay session
249 + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySession, expire);
250 + relaySession.onclose = function (sessionId) {
251 + // Remove the relay session
252 + delete relaySessions[sessionId];
253 + // If there are not more relay sessions, clear the cleanup timer
254 + if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; }
255 + }
256
222 - // Set the multi-tunnel session
223 - relaySessions[userid + '/' + req.session.x] = relaySession;
257 + // Set the multi-tunnel session
258 + relaySessions[webSessionId] = relaySession;
259
225 - // Setup the cleanup timer if needed
226 - if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); }
260 + // Setup the cleanup timer if needed
261 + if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); }
262
228 - // Redirect to root
229 - res.redirect('/');
263 + // Redirect to root
264 + res.redirect('/');
265 + });
266 });
267 }
268
webserver.js
+134 -77
@@ -3848,7 +3848,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3848 }
3849
3850 // Generate an old style cookie from the information in the database
3851 - var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey };
3851 + 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 };
3852 if (doc.userid) { cookie.uid = doc.userid; }
3853 if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
3854 if (doc.startTime != null) {
@@ -3870,7 +3870,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3870
3871 // Check the public id
3872 obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
3873 - // Check if any desktop sharing links are present, expire message.
3873 + // Check if any sharing links are present, expire message.
3874 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; }
3875
3876 // Search for the device share public identifier, expire message.
@@ -3886,22 +3886,43 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3886 // Check the start time, not yet valid message.
3887 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; }
3888
3889 - // Looks good, let's create the outbound session cookies.
3890 - // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
3891 - 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 };
3892 - if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
3893 - if (c.k != null) { authCookieData.k = c.k; }
3894 - const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
3895 -
3896 - // Server features
3897 - var features2 = 0;
3898 - if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
3899 -
3900 - // Lets respond by sending out the desktop viewer.
3901 - var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3902 - parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3903 - res.set({ 'Cache-Control': 'no-store' });
3904 - 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));
3889 + // If this is a web relay share, check if this feature is active
3890 + if ((c.p == 8) || (c.p == 16)) {
3891 + // This is a HTTP or HTTPS share
3892 + 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));
3893 + if (webRelayPort == 0) { res.sendStatus(404); return; }
3894 +
3895 + // Create the authentication cookie
3896 + 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 };
3897 + if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
3898 + const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
3899 +
3900 + // Redirect to a URL
3901 + var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
3902 + var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie;
3903 + if (c.addr != null) { url += '&addr=' + c.addr; }
3904 + if (c.pid != null) { url += '&relayid=' + c.pid; }
3905 + parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3906 + res.redirect(url);
3907 + } else {
3908 + // Looks good, let's create the outbound session cookies.
3909 + // This is a desktop, terminal or files share. We need to display the sharing page.
3910 + // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
3911 + 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 };
3912 + if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
3913 + if (c.k != null) { authCookieData.k = c.k; }
3914 + const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
3915 +
3916 + // Server features
3917 + var features2 = 0;
3918 + if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
3919 +
3920 + // Lets respond by sending out the desktop viewer.
3921 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3922 + parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3923 + res.set({ 'Cache-Control': 'no-store' });
3924 + 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));
3925 + }
3926 });
3927 });
3928 }
@@ -6549,32 +6570,56 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6570 parent.debug('web', 'webRelaySetup');
6571
6572 // Decode the relay cookie
6552 - if (req.query.c != null) {
6553 - // Decode and check if this relay cookie is valid
6554 - const urlCookie = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey);
6555 - if ((urlCookie != null) && (urlCookie.ruserid != null) && (urlCookie.x != null)) {
6556 - if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
6557 - if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
6558 - }
6573 + if (req.query.c == null) { res.sendStatus(404); return; }
6574 +
6575 + // Decode and check if this relay cookie is valid
6576 + var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire;
6577 + const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey);
6578 + if (urlCookie == null) { res.sendStatus(404); return; }
6579 +
6580 + // Decode the incomign cookie
6581 + if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
6582 + if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
6583 +
6584 + // This is a standard user, figure out what our web relay will be.
6585 + if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
6586 + if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
6587 + if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
6588 + userid = req.session.userid;
6589 + domainid = userid.split('/')[1];
6590 + domain = parent.config.domains[domainid];
6591 + nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
6592 + addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
6593 + port = parseInt(req.query.p);
6594 + appid = parseInt(req.query.appid);
6595 + webSessionId = req.session.userid + '/' + req.session.x;
6596 +
6597 + // Check that all the required arguments are present
6598 + 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; }
6599 + } else if (urlCookie.r == 8) {
6600 + // This is a guest user, figure out what our web relay will be.
6601 + userid = urlCookie.userid;
6602 + domainid = userid.split('/')[1];
6603 + domain = parent.config.domains[domainid];
6604 + nodeid = urlCookie.nid;
6605 + addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
6606 + port = urlCookie.port;
6607 + appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
6608 + webSessionId = userid + '/' + urlCookie.pid;
6609 + if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
6610 + if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
6611 + if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
6612 + expire = urlCookie.expire;
6613 }
6614
6561 - // Check that all the required arguments are present
6562 - 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; }
6563 -
6564 - // Get the user and domain information
6565 - const userid = req.session.userid;
6566 - const domainid = userid.split('/')[1];
6567 - const domain = parent.config.domains[domainid];
6568 - const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
6569 - const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
6570 - const port = parseInt(req.query.p);
6571 - const appid = parseInt(req.query.appid);
6615 + // No session identifier was setup, exit now
6616 + if (webSessionId == null) { res.sendStatus(404); return; }
6617
6618 // Check that we have an exact session on any of the relay DNS names
6619 var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
6620 for (var hostIndex in obj.args.relaydns) {
6621 const host = obj.args.relaydns[hostIndex];
6577 - xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + host;
6622 + xrelaySessionId = webSessionId + '/' + host;
6623 xrelaySession = webRelaySessions[xrelaySessionId];
6624 if (xrelaySession == null) {
6625 // 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
6654 }
6655 }
6656
6612 - // Check if there is a free relay DNS name we can use
6613 - var selectedHost = null;
6614 - if (freeRelayHost != null) {
6615 - // There is a free one, use it.
6616 - selectedHost = freeRelayHost;
6617 - } else {
6618 - // No free ones, close the oldest one
6619 - selectedHost = oldestRelayHost;
6620 - }
6621 - xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + selectedHost;
6657 + // Check that the user has rights to access this device
6658 + parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
6659 + // If there is no remote control rights, reject this web relay
6660 + if ((rights & 8) == 0) { res.sendStatus(404); return; }
6661
6623 - if (selectedHost == req.hostname) {
6624 - // If this web relay session id is not free, close it now
6625 - xrelaySession = webRelaySessions[xrelaySessionId];
6626 - if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
6627 -
6628 - // Create a web relay session
6629 - const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId);
6630 - relaySession.onclose = function (sessionId) {
6631 - // Remove the relay session
6632 - delete webRelaySessions[sessionId];
6633 - // If there are not more relay sessions, clear the cleanup timer
6634 - if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
6662 + // Check if there is a free relay DNS name we can use
6663 + var selectedHost = null;
6664 + if (freeRelayHost != null) {
6665 + // There is a free one, use it.
6666 + selectedHost = freeRelayHost;
6667 + } else {
6668 + // No free ones, close the oldest one
6669 + selectedHost = oldestRelayHost;
6670 }
6671 + xrelaySessionId = webSessionId + '/' + selectedHost;
6672 +
6673 + if (selectedHost == req.hostname) {
6674 + // If this web relay session id is not free, close it now
6675 + xrelaySession = webRelaySessions[xrelaySessionId];
6676 + if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
6677 +
6678 + // Create a web relay session
6679 + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire);
6680 + relaySession.onclose = function (sessionId) {
6681 + // Remove the relay session
6682 + delete webRelaySessions[sessionId];
6683 + // If there are not more relay sessions, clear the cleanup timer
6684 + if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
6685 + }
6686
6637 - // Set the multi-tunnel session
6638 - webRelaySessions[xrelaySessionId] = relaySession;
6687 + // Set the multi-tunnel session
6688 + webRelaySessions[xrelaySessionId] = relaySession;
6689
6640 - // Setup the cleanup timer if needed
6641 - if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6690 + // Setup the cleanup timer if needed
6691 + if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6692
6643 - // Redirect to root.
6644 - res.redirect('/');
6645 - } else {
6646 - if (req.query.noredirect != null) {
6647 - // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
6648 - res.sendStatus(404);
6693 + // Redirect to root.
6694 + res.redirect('/');
6695 } else {
6650 - // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
6651 - const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6652 - res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
6696 + if (req.query.noredirect != null) {
6697 + // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
6698 + res.sendStatus(404);
6699 + } else {
6700 + // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
6701 + const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6702 + res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
6703 + }
6704 }
6654 - }
6705 + });
6706 });
6707
6708 // Handle all incoming requests as web relays
@@ -6956,8 +7007,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7007
7008 // Handle an incoming request as a web relay
7009 function handleWebRelayRequest(req, res) {
6959 - if ((req.session.userid != null) && (req.session.x != null) && (obj.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) {
6960 - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x + '/' + req.hostname];
7010 + var webRelaySessionId = null;
7011 + if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
7012 + else if (req.session.z != null) { webRelaySessionId = req.session.z; }
7013 + if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
7014 + var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
7015 if (relaySession != null) {
7016 // The web relay session is valid, use it
7017 relaySession.handleRequest(req, res);
@@ -6973,8 +7027,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7027
7028 // Handle an incoming websocket connection as a web relay
7029 function handleWebRelayWebSocket(ws, req) {
6976 - if ((req.session.userid != null) && (req.session.x != null) && (obj.destroyedSessions[req.session.userid + '/' + req.session.x] == null)) {
6977 - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x + '/' + req.hostname];
7030 + var webRelaySessionId = null;
7031 + if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
7032 + else if (req.session.z != null) { webRelaySessionId = req.session.z; }
7033 + if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
7034 + var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
7035 if (relaySession != null) {
7036 // The multi-tunnel session is valid, use it
7037 relaySession.handleWebSocket(ws, req);