1
0
Fork 0
mirror of https://github.com/Ylianst/MeshCentral.git synced 2025-02-12 11:01:52 +00:00

Web relay improvements, #4172

This commit is contained in:
Ylian Saint-Hilaire 2022-06-25 16:22:51 -07:00
parent 0aeeb1c79c
commit 3b93d1adf7
2 changed files with 69 additions and 32 deletions

View file

@ -63,20 +63,27 @@ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// Construct a Web relay object // Construct a Web relay object
module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) { module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) {
const obj = {}; const obj = {};
obj.parent = parent;
obj.lastOperation = Date.now(); obj.lastOperation = Date.now();
obj.domain = domain;
obj.userid = userid; obj.userid = userid;
obj.nodeid = nodeid;
obj.addr = addr;
obj.port = port;
var pendingRequests = []; var pendingRequests = [];
var activeRequests = 0;
var nextTunnelId = 1; var nextTunnelId = 1;
var tunnels = {}; var tunnels = {};
// Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
obj.webCookie = null;
// Events // Events
obj.closed = false; obj.closed = false;
obj.onclose = null; obj.onclose = null;
// Handle new HTTP request // Handle new HTTP request
obj.handleRequest = function (req, res) { obj.handleRequest = function (req, res) {
console.log('handleRequest', req.url); //console.log('handleRequest', req.url);
pendingRequests.push([req, res]); pendingRequests.push([req, res]);
handleNextRequest(); handleNextRequest();
} }
@ -89,7 +96,7 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
count += (tunnels[i].isWebSocket ? 0 : 1); count += (tunnels[i].isWebSocket ? 0 : 1);
if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) { if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) {
// Found a free tunnel, use it // Found a free tunnel, use it
console.log('handleNextRequest-found empty tunnel'); //console.log('handleNextRequest-found empty tunnel');
const x = pendingRequests.shift(); const x = pendingRequests.shift();
tunnels[i].processRequest(x[0], x[1]); tunnels[i].processRequest(x[0], x[1]);
return; return;
@ -99,11 +106,11 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
if (count > 0) return; if (count > 0) return;
// Launch a new tunnel // Launch a new tunnel
console.log('handleNextRequest-starting new tunnel'); //console.log('handleNextRequest-starting new tunnel');
const tunnel = module.exports.CreateWebRelay(parent, db, args, domain); const tunnel = module.exports.CreateWebRelay(obj, db, args, domain);
tunnel.onclose = function (tunnelId) { console.log('tclose'); delete tunnels[tunnelId]; } tunnel.onclose = function (tunnelId) { delete tunnels[tunnelId]; }
tunnel.onconnect = function (tunnelId) { console.log('tconnect'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } tunnel.onconnect = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
tunnel.oncompleted = function (tunnelId) { console.log('tcompleted'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } tunnel.oncompleted = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
tunnel.connect(userid, nodeid, addr, port); tunnel.connect(userid, nodeid, addr, port);
tunnel.tunnelId = nextTunnelId++; tunnel.tunnelId = nextTunnelId++;
tunnels[tunnel.tunnelId] = tunnel; tunnels[tunnel.tunnelId] = tunnel;
@ -144,26 +151,27 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
obj.processRequest = function (req, res) { obj.processRequest = function (req, res) {
if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; } if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
console.log('processRequest-start', req.method); //console.log('processRequest-start', req.method);
// Construct the HTTP request // Construct the HTTP request
var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n'; var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
request += 'host: ' + obj.addr + ':' + obj.port + '\r\n'; request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
for (var i in req.headers) { const blockedHeaders = ['origin', 'host', 'cookie']; // These are headers we do not forward
const li = i.toLowerCase(); for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
if ((li != 'origin') && (li != 'host')) { request += i + ': ' + req.headers[i] + '\r\n'; } if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
}
request += '\r\n'; request += '\r\n';
//console.log('request', request);
if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) { if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) {
// Read the HTTP body and send the request to the device // Read the HTTP body and send the request to the device
obj.requestBinary = [Buffer.from(request)]; obj.requestBinary = [Buffer.from(request)];
req.on('data', function (data) { obj.requestBinary.push(data); }); req.on('data', function (data) { obj.requestBinary.push(data); });
req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; console.log('processRequest-sent-withbody'); }); req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; });
} else { } else {
// Request has no body, send it now // Request has no body, send it now
obj.wsClient.send(Buffer.from(request)); obj.wsClient.send(Buffer.from(request));
console.log('processRequest-sent-nobody'); //console.log('processRequest-sent-nobody');
} }
obj.res = res; obj.res = res;
} }
@ -173,6 +181,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
if (obj.closed == true) return; if (obj.closed == true) return;
obj.closed = true; obj.closed = true;
/*
// Event the session ending // Event the session ending
if ((obj.startTime) && (obj.meshid != null)) { if ((obj.startTime) && (obj.meshid != null)) {
// Collect how many raw bytes where received and sent. // Collect how many raw bytes where received and sent.
@ -187,6 +196,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
delete obj.startTime; delete obj.startTime;
delete obj.sessionid; delete obj.sessionid;
} }
*/
if (obj.wsClient) { if (obj.wsClient) {
obj.wsClient.removeAllListeners('open'); obj.wsClient.removeAllListeners('open');
obj.wsClient.removeAllListeners('message'); obj.wsClient.removeAllListeners('message');
@ -213,7 +223,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
// Encode a cookie for the mesh relay // Encode a cookie for the mesh relay
const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port }; const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port };
if (addr != null) { cookieContent.tcpaddr = addr; } if (addr != null) { cookieContent.tcpaddr = addr; }
const cookie = parent.encodeCookie(cookieContent, parent.loginCookieEncryptionKey); const cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
try { try {
// Setup the correct URL with domain and use TLS only if needed. // Setup the correct URL with domain and use TLS only if needed.
@ -222,9 +232,9 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
var domainadd = ''; var domainadd = '';
if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' } 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=' + cookie; // Protocol 14 is Web-TCP const url = protocol + '://localhost:' + args.port + '/' + domainadd + (((obj.mtype == 3) && (obj.relaynodeid == null)) ? 'local' : 'mesh') + 'relay.ashx?p=14&auth=' + cookie; // Protocol 14 is Web-TCP
parent.debug('relay', 'TCP: Connection websocket to ' + url); parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
obj.wsClient = new WebSocket(url, options); obj.wsClient = new WebSocket(url, options);
obj.wsClient.on('open', function () { parent.debug('relay', 'TCP: Relay websocket open'); }); 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. obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
if (obj.relayActive == false) { if (obj.relayActive == false) {
if ((data == 'c') || (data == 'cr')) { if ((data == 'c') || (data == 'cr')) {
@ -243,8 +253,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
processHttpData(data.toString('binary')); processHttpData(data.toString('binary'));
} }
}); });
obj.wsClient.on('close', function () { parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); }); obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
obj.wsClient.on('error', function (err) { parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); }); obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
} catch (ex) { } catch (ex) {
console.log(ex); console.log(ex);
} }
@ -310,10 +320,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
// This is a fully parsed HTTP response from the remote device // This is a fully parsed HTTP response from the remote device
function processHttpResponse(header, data) { function processHttpResponse(header, data) {
console.log('processHttpResponse'); //console.log('processHttpResponse', header);
obj.res.status(parseInt(header.Directive[1])); // Set the status obj.res.status(parseInt(header.Directive[1])); // Set the status
for (var i in header) { if (i != 'Directive') { obj.res.set(i, header[i]); } } // Set the headers const blockHeaders = ['Directive' ]; // These are headers we do not forward
for (var i in header) {
if (i == 'set-cookie') { parent.webCookie = header[i]; } // Keep the cookie, don't forward it
else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked
}
obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future
obj.res.end(data, 'binary'); // Write the data obj.res.end(data, 'binary'); // Write the data
delete obj.res; delete obj.res;
@ -329,7 +342,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
return true; return true;
} }
parent.debug('relay', 'TCP: Request for web relay'); parent.parent.debug('relay', 'TCP: Request for web relay');
return obj; return obj;
}; };

View file

@ -116,8 +116,15 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
} else { } else {
if ((req.session.userid != null) && (req.session.rid != null)) { if ((req.session.userid != null) && (req.session.rid != null)) {
var relayMultiTunnel = relayMultiTunnels[req.session.userid + '/' + req.session.rid]; var relayMultiTunnel = relayMultiTunnels[req.session.userid + '/' + req.session.rid];
if (relayMultiTunnel != null) { relayMultiTunnel.handleRequest(req, res); return; } if (relayMultiTunnel != null) {
// The multi-tunnel session is valid, use it
relayMultiTunnel.handleRequest(req, res);
} else {
// No multi-tunnel session with this relay identifier, close the HTTP request.
res.end();
}
} else { } else {
// The user is not logged in or does not have a relay identifier, close the HTTP request.
res.end(); res.end();
} }
} }
@ -136,15 +143,32 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
const userid = req.session.userid; const userid = req.session.userid;
const domainid = userid.split('/')[1]; const domainid = userid.split('/')[1];
const domain = parent.config.domains[domainid]; const domain = parent.config.domains[domainid];
const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
const port = parseInt(req.query.p);
// Create the multi-tunnel // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
const relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, ((req.query.relayid != null) ? req.query.relayid : req.query.n), (req.query.addr != null) ? req.query.addr : '127.0.0.1', parseInt(req.query.p)); var relayMultiTunnel = null;
relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; } for (var i in relayMultiTunnels) {
relayMultiTunnel.multiTunnelId = nextMultiTunnelId++; const xrelayMultiTunnel = relayMultiTunnels[i];
if ((xrelayMultiTunnel.domain.id == domain.id) && (xrelayMultiTunnel.userid == userid) && (xrelayMultiTunnel.nodeid == nodeid) && (xrelayMultiTunnel.addr == addr) && (xrelayMultiTunnel.port == port)) {
relayMultiTunnel = xrelayMultiTunnel; // We found an exact match
}
}
// Set the tunnel if (relayMultiTunnel != null) {
relayMultiTunnels[userid + '/' + relayMultiTunnel.multiTunnelId] = relayMultiTunnel; // Since we found a match, use it
req.session.rid = relayMultiTunnel.multiTunnelId; req.session.rid = relayMultiTunnel.multiTunnelId;
} else {
// Create the multi-tunnel
relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, nodeid, addr, port);
relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; }
relayMultiTunnel.multiTunnelId = nextMultiTunnelId++;
// Set the tunnel
relayMultiTunnels[userid + '/' + relayMultiTunnel.multiTunnelId] = relayMultiTunnel;
req.session.rid = relayMultiTunnel.multiTunnelId;
}
// Redirect to root // Redirect to root
res.redirect('/'); res.redirect('/');