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;