More work on web relay, #4172
This commit is contained in:
parent
4dd5c18db4
commit
571a0f1c2d
143
apprelays.js
143
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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 += '<a href=# onclick=p10WebRouter("' + node._id + '",1,80)>' + "HTTP" + '</a> ';
|
||||
//x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,443)>' + "HTTPS" + '</a> ';
|
||||
}
|
||||
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 += '<a href=# onclick=p10WebRouter("' + node._id + '",1,80)>' + "HTTP" + '</a> ';
|
||||
//x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,443)>' + "HTTPS" + '</a> ';
|
||||
}
|
||||
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];
|
||||
|
|
|
@ -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);
|
||||
|
|
13
webserver.js
13
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'; }
|
||||
|
|
Loading…
Reference in New Issue