diff --git a/apprelays.js b/apprelays.js
index d9ba6cbe..29dd0b41 100644
--- a/apprelays.js
+++ b/apprelays.js
@@ -19,7 +19,8 @@ Protocol numbers
10 = RDP
11 = SSH-TERM
12 = VNC
-13 - SSH-FILES
+13 = SSH-FILES
+14 = Web-TCP
*/
// Protocol Numbers
@@ -58,6 +59,146 @@ const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
+
+// Construct a TCP relay object
+module.exports.CreateTcpRelay = function (parent, db, req, args, domain) {
+ const Net = require('net');
+ const WebSocket = require('ws');
+
+ const obj = {};
+ obj.relayActive = false;
+ obj.closed = false;
+
+ // Events
+ obj.ondata = null;
+ obj.onconnect = null;
+ obj.onclose = null;
+
+ // Disconnect
+ obj.close = function (arg) {
+ if (obj.closed == true) return;
+ obj.closed = true;
+
+ // Event the session ending
+ if ((obj.startTime) && (obj.meshid != null)) {
+ // Collect how many raw bytes where received and sent.
+ // We sum both the websocket and TCP client in this case.
+ var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
+ if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
+ const sessionSeconds = Math.round((Date.now() - obj.startTime) / 1000);
+ const user = parent.users[obj.cookie.userid];
+ const username = (user != null) ? user.name : null;
+ const event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: obj.cookie.userid, username: username, sessionid: obj.sessionid, msgid: 123, msgArgs: [sessionSeconds, obj.sessionid], msg: "Left Web-SSH session \"" + obj.sessionid + "\" after " + sessionSeconds + " second(s).", protocol: PROTOCOL_WEBSSH, bytesin: inTraffc, bytesout: outTraffc };
+ parent.parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event);
+ delete obj.startTime;
+ delete obj.sessionid;
+ }
+ if (obj.wsClient) {
+ obj.wsClient.removeAllListeners('open');
+ obj.wsClient.removeAllListeners('message');
+ obj.wsClient.removeAllListeners('close');
+ try { obj.wsClient.close(); } catch (ex) { console.log(ex); }
+ delete obj.wsClient;
+ }
+
+ if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (ex) { console.log(ex); } } // Soft close, close the websocket
+ if (arg == 2) { try { ws._socket._parent.end(); } catch (ex) { console.log(ex); } } // Hard close, close the TCP socket
+ obj.ws.removeAllListeners();
+
+ // Event disconnection
+ if (obj.onclose) { obj.onclose(); }
+
+ obj.relayActive = false;
+ delete obj.cookie;
+ delete obj.nodeid;
+ delete obj.meshid;
+ delete obj.userid;
+ };
+
+ // Start the looppback server
+ function startRelayConnection() {
+ try {
+ // Setup the correct URL with domain and use TLS only if needed.
+ const options = { rejectUnauthorized: false };
+ const protocol = (args.tlsoffload) ? 'ws' : 'wss';
+ var domainadd = '';
+ if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
+ const url = protocol + '://localhost:' + args.port + '/' + domainadd + (((obj.mtype == 3) && (obj.relaynodeid == null)) ? 'local' : 'mesh') + 'relay.ashx?p=14&auth=' + obj.xcookie; // Protocol 14 is Web-TCP
+ parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
+ obj.wsClient = new WebSocket(url, options);
+ obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); });
+ obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
+ if (obj.relayActive == false) {
+ if ((data == 'c') || (data == 'cr')) {
+ obj.relayActive = true;
+ if (obj.onconnect) { obj.onconnect(); } // Event connection
+ }
+ } else {
+ if (typeof data == 'string') {
+ // Forward any ping/pong commands to the browser
+ var cmd = null;
+ try { cmd = JSON.parse(data); } catch (ex) { }
+ if ((cmd != null) && (cmd.ctrlChannel == '102938') && (cmd.type == 'ping')) { cmd.type = 'pong'; obj.wsClient.send(JSON.stringify(cmd)); }
+ return;
+ }
+ // Relay WS --> TCP, event data coming in
+ if (obj.ondata) { obj.ondata(data); }
+ }
+ });
+ obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
+ obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
+ } catch (ex) {
+ console.log(ex);
+ }
+ }
+
+ // Send data thru the relay tunnel
+ obj.send = function (data) {
+ if (obj.relayActive = - false) return false;
+ obj.wsClient.send(data);
+ return true;
+ }
+
+ parent.parent.debug('relay', 'TCP: Request for TCP relay (' + req.clientIp + ')');
+
+ // Decode the authentication cookie
+ obj.cookie = parent.parent.decodeCookie(req.query.auth, parent.parent.loginCookieEncryptionKey);
+ if ((obj.cookie == null) || (obj.cookie.userid == null) || (parent.users[obj.cookie.userid] == null)) { obj.ws.send(JSON.stringify({ action: 'sessionerror' })); obj.close(); return; }
+ obj.userid = obj.cookie.userid;
+
+ // Get the meshid for this device
+ parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) {
+ if (obj.cookie == null) return; // obj has been cleaned up, just exit.
+ if ((err != null) || (nodes == null) || (nodes.length != 1)) { parent.parent.debug('relay', 'TCP: Invalid device'); obj.close(); }
+ const node = nodes[0];
+ obj.nodeid = node._id; // Store the NodeID
+ obj.meshid = node.meshid; // Store the MeshID
+ obj.mtype = node.mtype; // Store the device group type
+
+ // Check if we need to relay thru a different agent
+ const mesh = parent.meshes[obj.meshid];
+ if (mesh && mesh.relayid) {
+ obj.relaynodeid = mesh.relayid;
+ obj.tcpaddr = node.host;
+
+ // Check if we have rights to the relayid device, does nothing if a relay is not used
+ checkRelayRights(parent, domain, obj.cookie.userid, obj.relaynodeid, function (allowed) {
+ if (obj.cookie == null) return; // obj has been cleaned up, just exit.
+ if (allowed !== true) { parent.parent.debug('relay', 'TCP: Attempt to use un-authorized relay'); obj.close(); return; }
+
+ // Re-encode a cookie with a device relay
+ const cookieContent = { userid: obj.cookie.userid, domainid: obj.cookie.domainid, nodeid: mesh.relayid, tcpaddr: node.host, tcpport: obj.cookie.tcpport };
+ obj.xcookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
+ });
+ } else {
+ obj.xcookie = req.query.auth;
+ }
+ });
+
+ return obj;
+};
+
+
// Construct a MSTSC Relay object, called upon connection
// This implementation does not have TLS support
// This is a bit of a hack as we are going to run the RDP connection thru a loopback connection.
diff --git a/meshrelay.js b/meshrelay.js
index d2915e67..aab96b60 100644
--- a/meshrelay.js
+++ b/meshrelay.js
@@ -43,6 +43,8 @@ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// 10 = Web-RDP
// 11 = Web-SSH
// 12 = Web-VNC
+// 13 = Web-SSH-Files
+// 14 = Web-TCP
// 100 = Intel AMT WSMAN
// 101 = Intel AMT Redirection
// 200 = Messenger
diff --git a/views/default.handlebars b/views/default.handlebars
index 4a685ffe..e6f688de 100644
--- a/views/default.handlebars
+++ b/views/default.handlebars
@@ -1449,6 +1449,7 @@
var features = parseInt('{{{features}}}');
var features2 = parseInt('{{{features2}}}');
var sessionTime = parseInt('{{{sessiontime}}}');
+ var webRelayPort = parseInt('{{{webRelayPort}}}');
var sessionRefreshTimer = null;
var domain = '{{{domain}}}';
var domainUrl = '{{{domainurl}}}';
@@ -2737,7 +2738,7 @@
if (message.name != null) { url += ('&name=' + encodeURIComponentEx(message.name)); }
if (message.ip != null) { url += ('&remoteip=' + message.ip); }
url += ('&appid=' + message.protocol + '&autoexit=1'); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP, 6 = MCRDesktop, 7 = MCRFiles
- console.log(url);
+ //console.log(url);
downloadFile(url, '');
} else if (message.tag == 'novnc') {
var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + (message.localRelay?'local':'mesh') + 'relay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key?('&key=' + urlargs.key):'') + '&l={{{lang}}}';
@@ -4573,6 +4574,10 @@
// RDP link, show this link only of the remote machine is Windows.
if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && (node.agent.id != 14)) {
+ if (webRelayPort != 0) {
+ x += '' + "HTTP" + ' ';
+ //x += '' + "HTTPS" + ' ';
+ }
if ((node.agent.id > 0) && (node.agent.id < 5)) {
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
@@ -7141,6 +7146,10 @@
// RDP link, show this link only of the remote machine is Windows.
if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
+ if (webRelayPort != 0) {
+ x += '' + "HTTP" + ' ';
+ //x += '' + "HTTPS" + ' ';
+ }
if ((node.agent.id > 0) && (node.agent.id < 5)) {
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
@@ -8063,6 +8072,22 @@
meshserver.send({ action: 'removedevices', nodeids: [ nodeid ] });
}
+ function p10WebRouter(nodeid, protocol, port, addr) {
+ var relayid = null;
+ var node = getNodeFromId(nodeid);
+ if (node.mtype == 3) { // Setup device relay if needed
+ var mesh = meshes[node.meshid];
+ if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
+ }
+ var servername = serverinfo.name;
+ if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
+ var url = 'https://' + servername + ':' + webRelayPort + '/control-redirect.ashx?n=' + nodeid + '&p=' + port + '&appid=' + protocol; // Protocol: 1 = HTTP, 2 = HTTPS
+ if (addr != null) { url += '&addr=' + addr; }
+ if (relayid != null) { url += '&relayid=' + relayid; }
+ safeNewWindow(url, 'WebRelay');
+ return false;
+ }
+
function p10MCRouter(nodeid, protocol, port, addr, localport) {
var node = getNodeFromId(nodeid);
var mesh = meshes[node.meshid];
diff --git a/webrelayserver.js b/webrelayserver.js
index 93b73556..bed4305c 100644
--- a/webrelayserver.js
+++ b/webrelayserver.js
@@ -24,11 +24,35 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
obj.net = require('net');
obj.app = obj.express();
obj.webRelayServer = null;
- obj.port = null;
+ obj.port = 0;
+ obj.relayTunnels = {} // RelayID --> Web Tunnel
const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
var tlsSessionStore = {}; // Store TLS session information for quick resume.
var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
+ if (args.trustedproxy) {
+ // Reverse proxy should add the "X-Forwarded-*" headers
+ try {
+ obj.app.set('trust proxy', args.trustedproxy);
+ } catch (ex) {
+ // If there is an error, try to resolve the string
+ if ((args.trustedproxy.length == 1) && (typeof args.trustedproxy[0] == 'string')) {
+ require('dns').lookup(args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.trustedproxy = [address]; } });
+ }
+ }
+ }
+ else if (typeof args.tlsoffload == 'object') {
+ // Reverse proxy should add the "X-Forwarded-*" headers
+ try {
+ obj.app.set('trust proxy', args.tlsoffload);
+ } catch (ex) {
+ // If there is an error, try to resolve the string
+ if ((Array.isArray(args.tlsoffload)) && (args.tlsoffload.length == 1) && (typeof args.tlsoffload[0] == 'string')) {
+ require('dns').lookup(args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.tlsoffload = [address]; } });
+ }
+ }
+ }
+
// Add HTTP security headers to all responses
obj.app.use(function (req, res, next) {
parent.debug('webrequest', req.url + ' (RelayServer)');
@@ -41,9 +65,50 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';"
});
+
+ // Set the real IP address of the request
+ // If a trusted reverse-proxy is sending us the remote IP address, use it.
+ var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
+ if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
+ if (
+ (args.trustedproxy === true) || (args.tlsoffload === true) ||
+ ((typeof args.trustedproxy == 'object') && (isIPMatch(ipex, args.trustedproxy))) ||
+ ((typeof args.tlsoffload == 'object') && (isIPMatch(ipex, args.tlsoffload)))
+ ) {
+ // Get client IP
+ if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
+ req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
+ } else if (req.headers['x-forwarded-for']) {
+ req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
+ } else if (req.headers['x-real-ip']) {
+ req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
+ } else {
+ req.clientIp = ipex;
+ }
+
+ // If there is a port number, remove it. This will only work for IPv4, but nice for people that have a bad reverse proxy config.
+ const clientIpSplit = req.clientIp.split(':');
+ if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
+
+ // Get server host
+ if (req.headers['x-forwarded-host']) { xforwardedhost = req.headers['x-forwarded-host'].split(',')[0]; } // If multiple hosts are specified with a comma, take the first one.
+ } else {
+ req.clientIp = ipex;
+ }
+
return next();
});
+ // This is the magic URL that will setup the relay session
+ obj.app.get('/control-redirect.ashx', function (req, res) {
+ res.set({ 'Cache-Control': 'no-store' });
+ parent.debug('web', 'webRelaySetup');
+
+ console.log('req.query', req.query);
+
+ res.redirect('/');
+ });
+
// Start the server, only after users and meshes are loaded from the database.
if (args.tlsoffload) {
// Setup the HTTP server without TLS
@@ -86,6 +151,7 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
obj.parent.updateServerState('http-relay-port', port);
if (args.aliasport != null) { obj.parent.updateServerState('http-relay-aliasport', args.aliasport); }
}
+ obj.port = port;
}
CheckListenPort(args.relayport, args.relayportbind, StartWebRelayServer);
diff --git a/webserver.js b/webserver.js
index b4b6be3e..10e033d7 100644
--- a/webserver.js
+++ b/webserver.js
@@ -2858,7 +2858,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
footer: (domain.footer == null) ? '' : domain.footer,
webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
amtscanoptions: amtscanoptions,
- pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports()
+ pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
+ webRelayPort: ((parent.webrelayserver != null) ? parent.webrelayserver.port : 0)
}, dbGetFunc.req, domain), user);
}
xdbGetFunc.req = req;
@@ -5846,11 +5847,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var selfurl = ' wss://' + req.headers.host;
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
+
+ // If the web relay port is enabled, allow the web page to redirect to it
+ var extraFrameSrc = '';
+ if ((parent.webrelayserver != null) && (parent.webrelayserver.port != 0)) {
+ extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
+ if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
+ }
+
const headers = {
'Referrer-Policy': 'no-referrer',
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
- 'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' mcrouter:; media-src 'self'; form-action 'self'"
+ 'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self'"
};
if (req.headers['user-agent'] && (req.headers['user-agent'].indexOf('Chrome') >= 0)) { headers['Permissions-Policy'] = 'interest-cohort=()'; } // Remove Google's FLoC Network, only send this if Chrome browser
if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }