First working web relay, very basic. #4172

Ylian Saint-Hilaire committed Jun 25, 2022 at 13:29 UTC 0aeeb1c79ce91e5a44d4b048152e5545fb359154
2 files changed +322 -141
apprelays.js
+195 -60
@@ -60,19 +60,113 @@ const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
60 const MESHRIGHT_ADMIN = 0xFFFFFFFF;
61
62
63 -// Construct a TCP relay object
64 -module.exports.CreateTcpRelay = function (parent, db, req, args, domain) {
65 - const Net = require('net');
66 - const WebSocket = require('ws');
63 +// Construct a Web relay object
64 +module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) {
65 + const obj = {};
66 + obj.lastOperation = Date.now();
67 + obj.userid = userid;
68 + var pendingRequests = [];
69 + var activeRequests = 0;
70 + var nextTunnelId = 1;
71 + var tunnels = {};
72 +
73 + // Events
74 + obj.closed = false;
75 + obj.onclose = null;
76 +
77 + // Handle new HTTP request
78 + obj.handleRequest = function (req, res) {
79 + console.log('handleRequest', req.url);
80 + pendingRequests.push([req, res]);
81 + handleNextRequest();
82 + }
83 +
84 + // Handle request
85 + function handleNextRequest() {
86 + // Check to see if any of the tunnels are free
87 + var count = 0;
88 + for (var i in tunnels) {
89 + count += (tunnels[i].isWebSocket ? 0 : 1);
90 + if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) {
91 + // Found a free tunnel, use it
92 + console.log('handleNextRequest-found empty tunnel');
93 + const x = pendingRequests.shift();
94 + tunnels[i].processRequest(x[0], x[1]);
95 + return;
96 + }
97 + }
98 +
99 + if (count > 0) return;
100 +
101 + // Launch a new tunnel
102 + console.log('handleNextRequest-starting new tunnel');
103 + const tunnel = module.exports.CreateWebRelay(parent, db, args, domain);
104 + tunnel.onclose = function (tunnelId) { console.log('tclose'); delete tunnels[tunnelId]; }
105 + tunnel.onconnect = function (tunnelId) { console.log('tconnect'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
106 + tunnel.oncompleted = function (tunnelId) { console.log('tcompleted'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
107 + tunnel.connect(userid, nodeid, addr, port);
108 + tunnel.tunnelId = nextTunnelId++;
109 + tunnels[tunnel.tunnelId] = tunnel;
110 + }
111 +
112 + // Close all tunnels
113 + function close() {
114 + if (obj.closed == true) return;
115 + obj.closed = true;
116 + for (var i in tunnels) { tunnels[i].close(); }
117 + tunnels = null;
118 + if (obj.onclose) { obj.onclose(obj.userid + '/' + obj.multiTunnelId); }
119 + delete obj.userid;
120 + delete obj.lastOperation;
121 + }
122 +
123 + return obj;
124 +}
125 +
126 +
127 +
128 +// Construct a Web relay object
129 +module.exports.CreateWebRelay = function (parent, db, args, domain) {
130 + //const Net = require('net');
131 + const WebSocket = require('ws')
132
133 const obj = {};
134 obj.relayActive = false;
135 obj.closed = false;
136 + obj.isWebSocket = false;
137
138 // Events
73 - obj.ondata = null;
74 - obj.onconnect = null;
139 obj.onclose = null;
140 + obj.oncompleted = null;
141 + obj.onconnect = null;
142 +
143 + // Process a HTTP request
144 + obj.processRequest = function (req, res) {
145 + if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
146 +
147 + console.log('processRequest-start', req.method);
148 +
149 + // Construct the HTTP request
150 + var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
151 + request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
152 + for (var i in req.headers) {
153 + const li = i.toLowerCase();
154 + if ((li != 'origin') && (li != 'host')) { request += i + ': ' + req.headers[i] + '\r\n'; }
155 + }
156 + request += '\r\n';
157 +
158 + if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) {
159 + // Read the HTTP body and send the request to the device
160 + obj.requestBinary = [Buffer.from(request)];
161 + req.on('data', function (data) { obj.requestBinary.push(data); });
162 + req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; console.log('processRequest-sent-withbody'); });
163 + } else {
164 + // Request has no body, send it now
165 + obj.wsClient.send(Buffer.from(request));
166 + console.log('processRequest-sent-nobody');
167 + }
168 + obj.res = res;
169 + }
170
171 // Disconnect
172 obj.close = function (arg) {
@@ -89,7 +183,7 @@ module.exports.CreateTcpRelay = function (parent, db, req, args, domain) {
183 const user = parent.users[obj.cookie.userid];
184 const username = (user != null) ? user.name : null;
185 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 };
92 - parent.parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event);
186 + parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event);
187 delete obj.startTime;
188 delete obj.sessionid;
189 }
@@ -101,37 +195,41 @@ module.exports.CreateTcpRelay = function (parent, db, req, args, domain) {
195 delete obj.wsClient;
196 }
197
104 - if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (ex) { console.log(ex); } } // Soft close, close the websocket
105 - if (arg == 2) { try { ws._socket._parent.end(); } catch (ex) { console.log(ex); } } // Hard close, close the TCP socket
106 - obj.ws.removeAllListeners();
198 + // Close any pending request
199 + if (obj.res) { obj.res.end(); delete obj.res; }
200
201 // Event disconnection
109 - if (obj.onclose) { obj.onclose(); }
202 + if (obj.onclose) { obj.onclose(obj.tunnelId); }
203
204 obj.relayActive = false;
112 - delete obj.cookie;
113 - delete obj.nodeid;
114 - delete obj.meshid;
115 - delete obj.userid;
205 };
206
207 // Start the looppback server
119 - function startRelayConnection() {
208 + obj.connect = function (userid, nodeid, addr, port) {
209 + if (obj.relayActive || obj.closed) return;
210 + obj.addr = addr;
211 + obj.port = port;
212 +
213 + // Encode a cookie for the mesh relay
214 + const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port };
215 + if (addr != null) { cookieContent.tcpaddr = addr; }
216 + const cookie = parent.encodeCookie(cookieContent, parent.loginCookieEncryptionKey);
217 +
218 try {
219 // Setup the correct URL with domain and use TLS only if needed.
220 const options = { rejectUnauthorized: false };
221 const protocol = (args.tlsoffload) ? 'ws' : 'wss';
222 var domainadd = '';
223 if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
126 - 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
127 - parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
224 + 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
225 + parent.debug('relay', 'TCP: Connection websocket to ' + url);
226 obj.wsClient = new WebSocket(url, options);
129 - obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); });
227 + obj.wsClient.on('open', function () { parent.debug('relay', 'TCP: Relay websocket open'); });
228 obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
229 if (obj.relayActive == false) {
230 if ((data == 'c') || (data == 'cr')) {
231 obj.relayActive = true;
134 - if (obj.onconnect) { obj.onconnect(); } // Event connection
232 + if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
233 }
234 } else {
235 if (typeof data == 'string') {
@@ -142,59 +240,96 @@ module.exports.CreateTcpRelay = function (parent, db, req, args, domain) {
240 return;
241 }
242 // Relay WS --> TCP, event data coming in
145 - if (obj.ondata) { obj.ondata(data); }
243 + processHttpData(data.toString('binary'));
244 }
245 });
148 - obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
149 - obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
246 + obj.wsClient.on('close', function () { parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
247 + obj.wsClient.on('error', function (err) { parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
248 } catch (ex) {
249 console.log(ex);
250 }
251 }
252
155 - // Send data thru the relay tunnel
156 - obj.send = function (data) {
157 - if (obj.relayActive = - false) return false;
158 - obj.wsClient.send(data);
159 - return true;
253 + // Process incoming HTTP data
254 + obj.socketAccumulator = '';
255 + obj.socketParseState = 0;
256 + function processHttpData(data) {
257 + obj.socketAccumulator += data;
258 + while (true) {
259 + //console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator);
260 + if (obj.socketParseState == 0) {
261 + var headersize = obj.socketAccumulator.indexOf('\r\n\r\n');
262 + if (headersize < 0) return;
263 + //obj.Debug("Header: "+obj.socketAccumulator.substring(0, headersize)); // Display received HTTP header
264 + obj.socketHeader = obj.socketAccumulator.substring(0, headersize).split('\r\n');
265 + obj.socketAccumulator = obj.socketAccumulator.substring(headersize + 4);
266 + obj.socketParseState = 1;
267 + obj.socketData = '';
268 + obj.socketXHeader = { Directive: obj.socketHeader[0].split(' ') };
269 + for (var i in obj.socketHeader) {
270 + if (i != 0) {
271 + var x2 = obj.socketHeader[i].indexOf(':');
272 + obj.socketXHeader[obj.socketHeader[i].substring(0, x2).toLowerCase()] = obj.socketHeader[i].substring(x2 + 2);
273 + }
274 + }
275 + }
276 + if (obj.socketParseState == 1) {
277 + var csize = -1;
278 + if ((obj.socketXHeader['connection'] != undefined) && (obj.socketXHeader['connection'].toLowerCase() == 'close') && ((obj.socketXHeader["transfer-encoding"] == undefined) || (obj.socketXHeader["transfer-encoding"].toLowerCase() != 'chunked'))) {
279 + // The body ends with a close, in this case, we will only process the header
280 + csize = 0;
281 + } else if (obj.socketXHeader['content-length'] != undefined) {
282 + // The body length is specified by the content-length
283 + csize = parseInt(obj.socketXHeader['content-length']);
284 + if (obj.socketAccumulator.length < csize) return;
285 + var data = obj.socketAccumulator.substring(0, csize);
286 + obj.socketAccumulator = obj.socketAccumulator.substring(csize);
287 + obj.socketData = data;
288 + csize = 0;
289 + } else {
290 + // The body is chunked
291 + var clen = obj.socketAccumulator.indexOf('\r\n');
292 + if (clen < 0) return; // Chunk length not found, exit now and get more data.
293 + // Chunk length if found, lets see if we can get the data.
294 + csize = parseInt(obj.socketAccumulator.substring(0, clen), 16);
295 + if (obj.socketAccumulator.length < clen + 2 + csize + 2) return;
296 + // We got a chunk with all of the data, handle the chunck now.
297 + var data = obj.socketAccumulator.substring(clen + 2, clen + 2 + csize);
298 + obj.socketAccumulator = obj.socketAccumulator.substring(clen + 2 + csize + 2);
299 + try { obj.socketData += data; } catch (ex) { console.log(ex, typeof data, data.length); }
300 + }
301 + if (csize == 0) {
302 + //obj.Debug("xxOnSocketData DONE: (" + obj.socketData.length + "): " + obj.socketData);
303 + processHttpResponse(obj.socketXHeader, obj.socketData);
304 + obj.socketParseState = 0;
305 + obj.socketHeader = null;
306 + }
307 + }
308 + }
309 }
310
162 - parent.parent.debug('relay', 'TCP: Request for TCP relay (' + req.clientIp + ')');
163 -
164 - // Decode the authentication cookie
165 - obj.cookie = parent.parent.decodeCookie(req.query.auth, parent.parent.loginCookieEncryptionKey);
166 - if ((obj.cookie == null) || (obj.cookie.userid == null) || (parent.users[obj.cookie.userid] == null)) { obj.ws.send(JSON.stringify({ action: 'sessionerror' })); obj.close(); return; }
167 - obj.userid = obj.cookie.userid;
168 -
169 - // Get the meshid for this device
170 - parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) {
171 - if (obj.cookie == null) return; // obj has been cleaned up, just exit.
172 - if ((err != null) || (nodes == null) || (nodes.length != 1)) { parent.parent.debug('relay', 'TCP: Invalid device'); obj.close(); }
173 - const node = nodes[0];
174 - obj.nodeid = node._id; // Store the NodeID
175 - obj.meshid = node.meshid; // Store the MeshID
176 - obj.mtype = node.mtype; // Store the device group type
311 + // This is a fully parsed HTTP response from the remote device
312 + function processHttpResponse(header, data) {
313 + console.log('processHttpResponse');
314
178 - // Check if we need to relay thru a different agent
179 - const mesh = parent.meshes[obj.meshid];
180 - if (mesh && mesh.relayid) {
181 - obj.relaynodeid = mesh.relayid;
182 - obj.tcpaddr = node.host;
315 + obj.res.status(parseInt(header.Directive[1])); // Set the status
316 + for (var i in header) { if (i != 'Directive') { obj.res.set(i, header[i]); } } // Set the headers
317 + 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
318 + obj.res.end(data, 'binary'); // Write the data
319 + delete obj.res;
320
184 - // Check if we have rights to the relayid device, does nothing if a relay is not used
185 - checkRelayRights(parent, domain, obj.cookie.userid, obj.relaynodeid, function (allowed) {
186 - if (obj.cookie == null) return; // obj has been cleaned up, just exit.
187 - if (allowed !== true) { parent.parent.debug('relay', 'TCP: Attempt to use un-authorized relay'); obj.close(); return; }
321 + // Event completion
322 + if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); }
323 + }
324
189 - // Re-encode a cookie with a device relay
190 - const cookieContent = { userid: obj.cookie.userid, domainid: obj.cookie.domainid, nodeid: mesh.relayid, tcpaddr: node.host, tcpport: obj.cookie.tcpport };
191 - obj.xcookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
192 - });
193 - } else {
194 - obj.xcookie = req.query.auth;
195 - }
196 - });
325 + // Send data thru the relay tunnel
326 + function send(data) {
327 + if (obj.relayActive = - false) return false;
328 + obj.wsClient.send(data);
329 + return true;
330 + }
331
332 + parent.debug('relay', 'TCP: Request for web relay');
333 return obj;
334 };
335
webrelayserver.js
+127 -81
@@ -19,109 +19,151 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
19 obj.parent = parent;
20 obj.db = db;
21 obj.express = require('express');
22 + obj.session = require('cookie-session');
23 obj.expressWs = null;
24 obj.tlsServer = null;
25 obj.net = require('net');
26 obj.app = obj.express();
27 obj.webRelayServer = null;
28 obj.port = 0;
28 - obj.relayTunnels = {} // RelayID --> Web Tunnel
29 + var nextMultiTunnelId = 1;
30 + var relayMultiTunnels = {} // RelayID --> Web Mutli-Tunnel
31 const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
32 var tlsSessionStore = {}; // Store TLS session information for quick resume.
33 var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
34
33 - if (args.trustedproxy) {
34 - // Reverse proxy should add the "X-Forwarded-*" headers
35 - try {
36 - obj.app.set('trust proxy', args.trustedproxy);
37 - } catch (ex) {
38 - // If there is an error, try to resolve the string
39 - if ((args.trustedproxy.length == 1) && (typeof args.trustedproxy[0] == 'string')) {
40 - require('dns').lookup(args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.trustedproxy = [address]; } });
35 + function serverStart() {
36 + if (args.trustedproxy) {
37 + // Reverse proxy should add the "X-Forwarded-*" headers
38 + try {
39 + obj.app.set('trust proxy', args.trustedproxy);
40 + } catch (ex) {
41 + // If there is an error, try to resolve the string
42 + if ((args.trustedproxy.length == 1) && (typeof args.trustedproxy[0] == 'string')) {
43 + require('dns').lookup(args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.trustedproxy = [address]; } });
44 + }
45 }
46 }
43 - }
44 - else if (typeof args.tlsoffload == 'object') {
45 - // Reverse proxy should add the "X-Forwarded-*" headers
46 - try {
47 - obj.app.set('trust proxy', args.tlsoffload);
48 - } catch (ex) {
49 - // If there is an error, try to resolve the string
50 - if ((Array.isArray(args.tlsoffload)) && (args.tlsoffload.length == 1) && (typeof args.tlsoffload[0] == 'string')) {
51 - require('dns').lookup(args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.tlsoffload = [address]; } });
47 + else if (typeof args.tlsoffload == 'object') {
48 + // Reverse proxy should add the "X-Forwarded-*" headers
49 + try {
50 + obj.app.set('trust proxy', args.tlsoffload);
51 + } catch (ex) {
52 + // If there is an error, try to resolve the string
53 + if ((Array.isArray(args.tlsoffload)) && (args.tlsoffload.length == 1) && (typeof args.tlsoffload[0] == 'string')) {
54 + require('dns').lookup(args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.tlsoffload = [address]; } });
55 + }
56 }
57 }
54 - }
58
56 - // Add HTTP security headers to all responses
57 - obj.app.use(function (req, res, next) {
58 - parent.debug('webrequest', req.url + ' (RelayServer)');
59 - res.removeHeader('X-Powered-By');
60 - res.set({
61 - 'strict-transport-security': 'max-age=60000; includeSubDomains',
62 - 'Referrer-Policy': 'no-referrer',
63 - 'x-frame-options': 'SAMEORIGIN',
64 - 'X-XSS-Protection': '1; mode=block',
65 - 'X-Content-Type-Options': 'nosniff',
66 - 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';"
67 - });
68 -
69 - // Set the real IP address of the request
70 - // If a trusted reverse-proxy is sending us the remote IP address, use it.
71 - var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
72 - if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
73 - if (
74 - (args.trustedproxy === true) || (args.tlsoffload === true) ||
75 - ((typeof args.trustedproxy == 'object') && (isIPMatch(ipex, args.trustedproxy))) ||
76 - ((typeof args.tlsoffload == 'object') && (isIPMatch(ipex, args.tlsoffload)))
77 - ) {
78 - // Get client IP
79 - if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
80 - req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
81 - } else if (req.headers['x-forwarded-for']) {
82 - req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
83 - } else if (req.headers['x-real-ip']) {
84 - req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
59 + // Setup cookie session
60 + var sessionOptions = {
61 + name: 'xid', // Recommended security practice to not use the default cookie name
62 + httpOnly: true,
63 + keys: [args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
64 + secure: (args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
65 + sameSite: args.sessionsamesite
66 + }
67 + if (args.sessiontime != null) { sessionOptions.maxAge = (args.sessiontime * 60 * 1000); }
68 + obj.app.use(obj.session(sessionOptions));
69 +
70 + // Add HTTP security headers to all responses
71 + obj.app.use(function (req, res, next) {
72 + parent.debug('webrequest', req.url + ' (RelayServer)');
73 + res.removeHeader('X-Powered-By');
74 + res.set({
75 + 'strict-transport-security': 'max-age=60000; includeSubDomains',
76 + 'Referrer-Policy': 'no-referrer',
77 + 'x-frame-options': 'SAMEORIGIN',
78 + 'X-XSS-Protection': '1; mode=block',
79 + 'X-Content-Type-Options': 'nosniff',
80 + 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';"
81 + });
82 +
83 + // Set the real IP address of the request
84 + // If a trusted reverse-proxy is sending us the remote IP address, use it.
85 + var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
86 + if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
87 + if (
88 + (args.trustedproxy === true) || (args.tlsoffload === true) ||
89 + ((typeof args.trustedproxy == 'object') && (isIPMatch(ipex, args.trustedproxy))) ||
90 + ((typeof args.tlsoffload == 'object') && (isIPMatch(ipex, args.tlsoffload)))
91 + ) {
92 + // Get client IP
93 + if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
94 + req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
95 + } else if (req.headers['x-forwarded-for']) {
96 + req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
97 + } else if (req.headers['x-real-ip']) {
98 + req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
99 + } else {
100 + req.clientIp = ipex;
101 + }
102 +
103 + // 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.
104 + const clientIpSplit = req.clientIp.split(':');
105 + if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
106 +
107 + // Get server host
108 + 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.
109 } else {
110 req.clientIp = ipex;
111 }
112
89 - // 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.
90 - const clientIpSplit = req.clientIp.split(':');
91 - if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
113 + // Check if this there is a multi-tunnel for this request
114 + if (req.url.startsWith('/control-redirect.ashx?n=')) {
115 + return next();
116 + } else {
117 + if ((req.session.userid != null) && (req.session.rid != null)) {
118 + var relayMultiTunnel = relayMultiTunnels[req.session.userid + '/' + req.session.rid];
119 + if (relayMultiTunnel != null) { relayMultiTunnel.handleRequest(req, res); return; }
120 + } else {
121 + res.end();
122 + }
123 + }
124 + });
125 +
126 + // This is the magic URL that will setup the relay session
127 + obj.app.get('/control-redirect.ashx', function (req, res) {
128 + if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
129 + res.set({ 'Cache-Control': 'no-store' });
130 + parent.debug('web', 'webRelaySetup');
131
93 - // Get server host
94 - 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.
132 + // Check that all the required arguments are present
133 + if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
134 +
135 + // Get the user and domain information
136 + const userid = req.session.userid;
137 + const domainid = userid.split('/')[1];
138 + const domain = parent.config.domains[domainid];
139 +
140 + // Create the multi-tunnel
141 + 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));
142 + relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; }
143 + relayMultiTunnel.multiTunnelId = nextMultiTunnelId++;
144 +
145 + // Set the tunnel
146 + relayMultiTunnels[userid + '/' + relayMultiTunnel.multiTunnelId] = relayMultiTunnel;
147 + req.session.rid = relayMultiTunnel.multiTunnelId;
148 +
149 + // Redirect to root
150 + res.redirect('/');
151 + });
152 +
153 + // Start the server, only after users and meshes are loaded from the database.
154 + if (args.tlsoffload) {
155 + // Setup the HTTP server without TLS
156 + obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
157 } else {
96 - req.clientIp = ipex;
158 + // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
159 + 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 };
160 + obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
161 + obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
162 + obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
163 + obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
164 + obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
165 + obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
166 }
98 -
99 - return next();
100 - });
101 -
102 - // This is the magic URL that will setup the relay session
103 - obj.app.get('/control-redirect.ashx', function (req, res) {
104 - res.set({ 'Cache-Control': 'no-store' });
105 - parent.debug('web', 'webRelaySetup');
106 -
107 - console.log('req.query', req.query);
108 -
109 - res.redirect('/');
110 - });
111 -
112 - // Start the server, only after users and meshes are loaded from the database.
113 - if (args.tlsoffload) {
114 - // Setup the HTTP server without TLS
115 - obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
116 - } else {
117 - // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
118 - 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 };
119 - obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
120 - obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
121 - obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
122 - obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
123 - obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
124 - obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
167 }
168
169 // Find a free port starting with the specified one and going up.
@@ -154,6 +196,10 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
196 obj.port = port;
197 }
198
199 + function getRandomPassword() { return Buffer.from(require('crypto').randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
200 +
201 + // Start up the web relay server
202 + serverStart();
203 CheckListenPort(args.relayport, args.relayportbind, StartWebRelayServer);
204
205 return obj;