From 0aeeb1c79ce91e5a44d4b048152e5545fb359154 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 25 Jun 2022 13:29:24 -0700 Subject: [PATCH] First working web relay, very basic. #4172 --- apprelays.js | 255 +++++++++++++++++++++++++++++++++++----------- webrelayserver.js | 206 ++++++++++++++++++++++--------------- 2 files changed, 321 insertions(+), 140 deletions(-) diff --git a/apprelays.js b/apprelays.js index 29dd0b41..8d9b857e 100644 --- a/apprelays.js +++ b/apprelays.js @@ -60,19 +60,113 @@ 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'); +// Construct a Web relay object +module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) { + const obj = {}; + obj.lastOperation = Date.now(); + obj.userid = userid; + var pendingRequests = []; + var activeRequests = 0; + var nextTunnelId = 1; + var tunnels = {}; + + // Events + obj.closed = false; + obj.onclose = null; + + // Handle new HTTP request + obj.handleRequest = function (req, res) { + console.log('handleRequest', req.url); + pendingRequests.push([req, res]); + handleNextRequest(); + } + + // Handle request + function handleNextRequest() { + // Check to see if any of the tunnels are free + var count = 0; + for (var i in tunnels) { + count += (tunnels[i].isWebSocket ? 0 : 1); + if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) { + // Found a free tunnel, use it + console.log('handleNextRequest-found empty tunnel'); + const x = pendingRequests.shift(); + tunnels[i].processRequest(x[0], x[1]); + return; + } + } + + if (count > 0) return; + + // Launch a new tunnel + console.log('handleNextRequest-starting new tunnel'); + const tunnel = module.exports.CreateWebRelay(parent, db, args, domain); + tunnel.onclose = function (tunnelId) { console.log('tclose'); 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.oncompleted = function (tunnelId) { console.log('tcompleted'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } + tunnel.connect(userid, nodeid, addr, port); + tunnel.tunnelId = nextTunnelId++; + tunnels[tunnel.tunnelId] = tunnel; + } + + // Close all tunnels + function close() { + if (obj.closed == true) return; + obj.closed = true; + for (var i in tunnels) { tunnels[i].close(); } + tunnels = null; + if (obj.onclose) { obj.onclose(obj.userid + '/' + obj.multiTunnelId); } + delete obj.userid; + delete obj.lastOperation; + } + + return obj; +} + + + +// Construct a Web relay object +module.exports.CreateWebRelay = function (parent, db, args, domain) { + //const Net = require('net'); + const WebSocket = require('ws') const obj = {}; obj.relayActive = false; obj.closed = false; + obj.isWebSocket = false; // Events - obj.ondata = null; - obj.onconnect = null; obj.onclose = null; + obj.oncompleted = null; + obj.onconnect = null; + + // Process a HTTP request + obj.processRequest = function (req, res) { + if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; } + + console.log('processRequest-start', req.method); + + // Construct the HTTP request + var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n'; + request += 'host: ' + obj.addr + ':' + obj.port + '\r\n'; + for (var i in req.headers) { + const li = i.toLowerCase(); + if ((li != 'origin') && (li != 'host')) { request += i + ': ' + req.headers[i] + '\r\n'; } + } + request += '\r\n'; + + if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) { + // Read the HTTP body and send the request to the device + obj.requestBinary = [Buffer.from(request)]; + 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'); }); + } else { + // Request has no body, send it now + obj.wsClient.send(Buffer.from(request)); + console.log('processRequest-sent-nobody'); + } + obj.res = res; + } // Disconnect obj.close = function (arg) { @@ -89,7 +183,7 @@ module.exports.CreateTcpRelay = function (parent, db, req, args, domain) { 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); + parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event); delete obj.startTime; delete obj.sessionid; } @@ -101,37 +195,41 @@ module.exports.CreateTcpRelay = function (parent, db, req, args, domain) { 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(); + // Close any pending request + if (obj.res) { obj.res.end(); delete obj.res; } // Event disconnection - if (obj.onclose) { obj.onclose(); } + if (obj.onclose) { obj.onclose(obj.tunnelId); } obj.relayActive = false; - delete obj.cookie; - delete obj.nodeid; - delete obj.meshid; - delete obj.userid; }; // Start the looppback server - function startRelayConnection() { + obj.connect = function (userid, nodeid, addr, port) { + if (obj.relayActive || obj.closed) return; + obj.addr = addr; + obj.port = port; + + // Encode a cookie for the mesh relay + const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port }; + if (addr != null) { cookieContent.tcpaddr = addr; } + const cookie = parent.encodeCookie(cookieContent, parent.loginCookieEncryptionKey); + 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); + 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); obj.wsClient = new WebSocket(url, options); - obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); }); + obj.wsClient.on('open', function () { 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 + if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection } } else { if (typeof data == 'string') { @@ -142,59 +240,96 @@ module.exports.CreateTcpRelay = function (parent, db, req, args, domain) { return; } // Relay WS --> TCP, event data coming in - if (obj.ondata) { obj.ondata(data); } + processHttpData(data.toString('binary')); } }); - 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(); }); + obj.wsClient.on('close', function () { 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(); }); } catch (ex) { console.log(ex); } } + // Process incoming HTTP data + obj.socketAccumulator = ''; + obj.socketParseState = 0; + function processHttpData(data) { + obj.socketAccumulator += data; + while (true) { + //console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator); + if (obj.socketParseState == 0) { + var headersize = obj.socketAccumulator.indexOf('\r\n\r\n'); + if (headersize < 0) return; + //obj.Debug("Header: "+obj.socketAccumulator.substring(0, headersize)); // Display received HTTP header + obj.socketHeader = obj.socketAccumulator.substring(0, headersize).split('\r\n'); + obj.socketAccumulator = obj.socketAccumulator.substring(headersize + 4); + obj.socketParseState = 1; + obj.socketData = ''; + obj.socketXHeader = { Directive: obj.socketHeader[0].split(' ') }; + for (var i in obj.socketHeader) { + if (i != 0) { + var x2 = obj.socketHeader[i].indexOf(':'); + obj.socketXHeader[obj.socketHeader[i].substring(0, x2).toLowerCase()] = obj.socketHeader[i].substring(x2 + 2); + } + } + } + if (obj.socketParseState == 1) { + var csize = -1; + if ((obj.socketXHeader['connection'] != undefined) && (obj.socketXHeader['connection'].toLowerCase() == 'close') && ((obj.socketXHeader["transfer-encoding"] == undefined) || (obj.socketXHeader["transfer-encoding"].toLowerCase() != 'chunked'))) { + // The body ends with a close, in this case, we will only process the header + csize = 0; + } else if (obj.socketXHeader['content-length'] != undefined) { + // The body length is specified by the content-length + csize = parseInt(obj.socketXHeader['content-length']); + if (obj.socketAccumulator.length < csize) return; + var data = obj.socketAccumulator.substring(0, csize); + obj.socketAccumulator = obj.socketAccumulator.substring(csize); + obj.socketData = data; + csize = 0; + } else { + // The body is chunked + var clen = obj.socketAccumulator.indexOf('\r\n'); + if (clen < 0) return; // Chunk length not found, exit now and get more data. + // Chunk length if found, lets see if we can get the data. + csize = parseInt(obj.socketAccumulator.substring(0, clen), 16); + if (obj.socketAccumulator.length < clen + 2 + csize + 2) return; + // We got a chunk with all of the data, handle the chunck now. + var data = obj.socketAccumulator.substring(clen + 2, clen + 2 + csize); + obj.socketAccumulator = obj.socketAccumulator.substring(clen + 2 + csize + 2); + try { obj.socketData += data; } catch (ex) { console.log(ex, typeof data, data.length); } + } + if (csize == 0) { + //obj.Debug("xxOnSocketData DONE: (" + obj.socketData.length + "): " + obj.socketData); + processHttpResponse(obj.socketXHeader, obj.socketData); + obj.socketParseState = 0; + obj.socketHeader = null; + } + } + } + } + + // This is a fully parsed HTTP response from the remote device + function processHttpResponse(header, data) { + console.log('processHttpResponse'); + + 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 + 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 + delete obj.res; + + // Event completion + if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); } + } + // Send data thru the relay tunnel - obj.send = function (data) { + function send(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; - } - }); - + parent.debug('relay', 'TCP: Request for web relay'); return obj; }; diff --git a/webrelayserver.js b/webrelayserver.js index bed4305c..274f8829 100644 --- a/webrelayserver.js +++ b/webrelayserver.js @@ -19,109 +19,151 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, obj.parent = parent; obj.db = db; obj.express = require('express'); + obj.session = require('cookie-session'); obj.expressWs = null; obj.tlsServer = null; obj.net = require('net'); obj.app = obj.express(); obj.webRelayServer = null; obj.port = 0; - obj.relayTunnels = {} // RelayID --> Web Tunnel + var nextMultiTunnelId = 1; + var relayMultiTunnels = {} // RelayID --> Web Mutli-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]; } }); + function serverStart() { + 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]; } }); + 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)'); - res.removeHeader('X-Powered-By'); - res.set({ - 'strict-transport-security': 'max-age=60000; includeSubDomains', - 'Referrer-Policy': 'no-referrer', - 'x-frame-options': 'SAMEORIGIN', - 'X-XSS-Protection': '1; mode=block', - 'X-Content-Type-Options': 'nosniff', - 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';" - }); + // Setup cookie session + var sessionOptions = { + name: 'xid', // Recommended security practice to not use the default cookie name + httpOnly: true, + keys: [args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances + secure: (args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) + sameSite: args.sessionsamesite + } + if (args.sessiontime != null) { sessionOptions.maxAge = (args.sessiontime * 60 * 1000); } + obj.app.use(obj.session(sessionOptions)); - // 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(); + // Add HTTP security headers to all responses + obj.app.use(function (req, res, next) { + parent.debug('webrequest', req.url + ' (RelayServer)'); + res.removeHeader('X-Powered-By'); + res.set({ + 'strict-transport-security': 'max-age=60000; includeSubDomains', + 'Referrer-Policy': 'no-referrer', + 'x-frame-options': 'SAMEORIGIN', + 'X-XSS-Protection': '1; mode=block', + '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; } - // 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]; } + // Check if this there is a multi-tunnel for this request + if (req.url.startsWith('/control-redirect.ashx?n=')) { + return next(); + } else { + if ((req.session.userid != null) && (req.session.rid != null)) { + var relayMultiTunnel = relayMultiTunnels[req.session.userid + '/' + req.session.rid]; + if (relayMultiTunnel != null) { relayMultiTunnel.handleRequest(req, res); return; } + } else { + res.end(); + } + } + }); - // 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. + // This is the magic URL that will setup the relay session + obj.app.get('/control-redirect.ashx', function (req, res) { + if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; } + res.set({ 'Cache-Control': 'no-store' }); + parent.debug('web', 'webRelaySetup'); + + // Check that all the required arguments are present + if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; } + + // Get the user and domain information + const userid = req.session.userid; + const domainid = userid.split('/')[1]; + const domain = parent.config.domains[domainid]; + + // Create the multi-tunnel + 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)); + 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 + res.redirect('/'); + }); + + // Start the server, only after users and meshes are loaded from the database. + if (args.tlsoffload) { + // Setup the HTTP server without TLS + obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } }); } else { - req.clientIp = ipex; + // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS). + const tlsOptions = { cert: certificates.web.cert, key: certificates.web.key, ca: certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }; + obj.tlsServer = require('https').createServer(tlsOptions, obj.app); + obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ }); + obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); }); + obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); }); + obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); }); + obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } }); } - - 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 - obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } }); - } else { - // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS). - const tlsOptions = { cert: certificates.web.cert, key: certificates.web.key, ca: certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }; - obj.tlsServer = require('https').createServer(tlsOptions, obj.app); - obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ }); - obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); }); - obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); }); - obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); }); - obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } }); } // Find a free port starting with the specified one and going up. @@ -154,6 +196,10 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, obj.port = port; } + function getRandomPassword() { return Buffer.from(require('crypto').randomBytes(9), 'binary').toString('base64').split('/').join('@'); } + + // Start up the web relay server + serverStart(); CheckListenPort(args.relayport, args.relayportbind, StartWebRelayServer); return obj;