Fixed web relay for responses with no body.
Ylian Saint-Hilaire committed
Jun 27, 2022 at 23:08 UTC
91dead8e84af7d99d4d22b1e0942cee83d6e52a1
2 files changed
+148
-57
apprelays.js
+109
-38
@@ -109,7 +109,13 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
109
110
// Handle new HTTP request
111
obj.handleRequest = function (req, res) {
112
- pendingRequests.push([req, res]);
112
+ pendingRequests.push([req, res, false]);
113
+ handleNextRequest();
114
+ }
115
+
116
+ // Handle new websocket request
117
+ obj.handleWebSocket = function (ws, req) {
118
+ pendingRequests.push([req, ws, true]);
119
handleNextRequest();
120
}
121
@@ -119,16 +125,19 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
125
var count = 0;
126
for (var i in tunnels) {
127
count += (tunnels[i].isWebSocket ? 0 : 1);
122
- if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) {
128
+ if ((tunnels[i].relayActive == true) && (tunnels[i].res == null) && (tunnels[i].isWebSocket == false)) {
129
// Found a free tunnel, use it
130
const x = pendingRequests.shift();
125
- tunnels[i].processRequest(x[0], x[1]);
131
+ if (x[2] == true) { tunnels[i].processWebSocket(x[0], x[1]); } else { tunnels[i].processRequest(x[0], x[1]); }
132
return;
133
}
134
}
135
136
if (count > 0) return;
137
+ launchNewTunnel();
138
+ }
139
140
+ function launchNewTunnel() {
141
// Launch a new tunnel
142
const tunnel = module.exports.CreateWebRelay(obj, db, args, domain);
143
tunnel.onclose = function (tunnelId) {
@@ -136,17 +145,20 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
145
// Count how many non-websocket tunnels are active
146
var count = 0;
147
for (var i in tunnels) { count += (tunnels[i].isWebSocket ? 0 : 1); }
139
- // If there are none, discard all pending HTTP requests
140
- if (count == 0) {
141
- for (var i in pendingRequests) {
142
- const x = pendingRequests[i];
143
- if (x != null) { x[1].end(); }
144
- pendingRequests = [];
145
- }
148
+ if (count == 0) { launchNewTunnel(); }
149
+ }
150
+ tunnel.onconnect = function (tunnelId) {
151
+ if (pendingRequests.length > 0) {
152
+ const x = pendingRequests.shift();
153
+ if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
154
+ }
155
+ }
156
+ tunnel.oncompleted = function (tunnelId) {
157
+ if (pendingRequests.length > 0) {
158
+ const x = pendingRequests.shift();
159
+ if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
160
}
161
}
148
- tunnel.onconnect = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
149
- tunnel.oncompleted = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
162
tunnel.connect(userid, nodeid, addr, port, appid);
163
tunnel.tunnelId = nextTunnelId++;
164
tunnels[tunnel.tunnelId] = tunnel;
@@ -215,6 +227,26 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
227
obj.res = res;
228
}
229
230
+ // Process a websocket request
231
+ obj.processWebSocket = function (req, ws) {
232
+ //console.log('processWebSocket', req.url);
233
+ if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
234
+ parent.lastOperation = obj.lastOperation = Date.now();
235
+
236
+ // Mark this tunnel as being a web socket tunnel
237
+ obj.isWebSocket = true;
238
+ obj.ws = ws;
239
+
240
+ // Construct the HTTP request and send it out
241
+ var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
242
+ request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
243
+ const blockedHeaders = ['origin', 'host', 'cookie']; // These are headers we do not forward
244
+ for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
245
+ if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
246
+ request += '\r\n';
247
+ send(Buffer.from(request));
248
+ }
249
+
250
// Disconnect
251
obj.close = function (arg) {
252
if (obj.closed == true) return;
@@ -251,6 +283,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
283
284
// Close any pending request
285
if (obj.res) { obj.res.end(); delete obj.res; }
286
+ if (obj.ws) { obj.ws.close(); delete obj.ws; }
287
288
// Event disconnection
289
if (obj.onclose) { obj.onclose(obj.tunnelId); }
@@ -353,7 +386,6 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
386
//obj.Debug("Header: "+obj.socketAccumulator.substring(0, headersize)); // Display received HTTP header
387
obj.socketHeader = obj.socketAccumulator.substring(0, headersize).split('\r\n');
388
obj.socketAccumulator = obj.socketAccumulator.substring(headersize + 4);
356
- obj.socketParseState = 1;
389
obj.socketXHeader = { Directive: obj.socketHeader[0].split(' ') };
390
for (var i in obj.socketHeader) {
391
if (i != 0) {
@@ -361,11 +393,18 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
393
obj.socketXHeader[obj.socketHeader[i].substring(0, x2).toLowerCase()] = obj.socketHeader[i].substring(x2 + 2);
394
}
395
}
364
- processHttpResponse(obj.socketXHeader, null, false);
396
+
397
+ // Check if this HTTP request has a body
398
+ if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) { obj.socketParseState = 1; }
399
+ if (obj.socketXHeader['content-length'] != null) { obj.socketParseState = 1; }
400
+ if ((obj.socketXHeader["transfer-encoding"] != null) && (obj.socketXHeader["transfer-encoding"].toLowerCase() == 'chunked')) { obj.socketParseState = 1; }
401
+
402
+ // Forward the HTTP request into the tunnel, if no body is present, close the request.
403
+ processHttpResponse(obj.socketXHeader, null, (obj.socketParseState == 0));
404
}
405
if (obj.socketParseState == 1) {
406
var csize = -1;
368
- if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close') && ((obj.socketXHeader["transfer-encoding"] == null) || (obj.socketXHeader["transfer-encoding"].toLowerCase() != 'chunked'))) {
407
+ if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) {
408
// The body ends with a close, in this case, we will only process the header
409
processHttpResponse(null, null, true);
410
csize = 0;
@@ -378,10 +417,10 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
417
processHttpResponse(null, data, (obj.socketContentLengthRemaining == 0)); // Send any data we have, if we are done, signal the end of the response
418
if (obj.socketContentLengthRemaining > 0) return; // If more data is needed, return now so we exit the while() loop.
419
csize = 0; // We are done
381
- } else {
420
+ } else if ((obj.socketXHeader["transfer-encoding"] != null) && (obj.socketXHeader["transfer-encoding"].toLowerCase() == 'chunked')) {
421
// The body is chunked
422
var clen = obj.socketAccumulator.indexOf('\r\n');
384
- if (clen < 0) return; // Chunk length not found, exit now and get more data.
423
+ if (clen < 0) { return; } // Chunk length not found, exit now and get more data.
424
// Chunk length if found, lets see if we can get the data.
425
csize = parseInt(obj.socketAccumulator.substring(0, clen), 16);
426
if (obj.socketAccumulator.length < clen + 2 + csize + 2) return;
@@ -396,36 +435,68 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
435
obj.socketHeader = null;
436
}
437
}
438
+ if (obj.socketParseState == 2) {
439
+ // We are in websocket pass-thru mode, decode the websocket frame
440
+ if (obj.socketAccumulator.length < 2) return; // Need at least 2 bytes to decode a websocket header
441
+ console.log('WebSocket frame', obj.socketAccumulator.length, Buffer.from(obj.socketAccumulator, 'binary'));
442
+
443
+ const buf = Buffer.from(obj.socketAccumulator, 'binary');
444
+ const fin = ((buf[0] & 0x80) != 0);
445
+ const op = buf[0] & 0x0F;
446
+ const mask = ((buf[1] & 0x80) != 0);
447
+ const len = buf[1] & 0x7F;
448
+ console.log('fin', fin);
449
+ console.log('op', op);
450
+ console.log('mask', mask);
451
+ console.log('len', len);
452
+
453
+ // Connection close
454
+ if ((fin == true) || (op == 8)) { obj.close(); }
455
+
456
+ return;
457
+ }
458
}
459
}
460
461
// This is a fully parsed HTTP response from the remote device
462
function processHttpResponse(header, data, done) {
404
- if (obj.res == null) return;
405
- parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
406
-
407
- // If there is a header, send it
408
- if (header != null) {
409
- obj.res.status(parseInt(header.Directive[1])); // Set the status
410
- const blockHeaders = ['Directive']; // These are headers we do not forward
411
- for (var i in header) {
412
- if (i == 'set-cookie') { parent.webCookie = header[i]; } // Keep the cookie, don't forward it
413
- else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked
463
+ //console.log('processHttpResponse');
464
+ if (obj.isWebSocket == false) {
465
+ if (obj.res == null) return;
466
+ parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
467
+
468
+ // If there is a header, send it
469
+ if (header != null) {
470
+ obj.res.status(parseInt(header.Directive[1])); // Set the status
471
+ const blockHeaders = ['Directive']; // These are headers we do not forward
472
+ for (var i in header) {
473
+ if (i == 'set-cookie') { parent.webCookie = header[i]; } // Keep the cookie, don't forward it
474
+ else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked
475
+ }
476
+ 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
477
}
415
- 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
416
- }
478
418
- // If there is data, send it
419
- if (data != null) { obj.res.write(data, 'binary'); }
479
+ // If there is data, send it
480
+ if (data != null) { obj.res.write(data, 'binary'); }
481
421
- // If we are done, close the response
422
- if (done == true) {
423
- // Close the response
424
- obj.res.end();
425
- delete obj.res;
482
+ // If we are done, close the response
483
+ if (done == true) {
484
+ // Close the response
485
+ obj.res.end();
486
+ delete obj.res;
487
427
- // Event completion
428
- if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); }
488
+ // Event completion
489
+ if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); }
490
+ }
491
+ } else {
492
+ // Tunnel is now in web socket pass-thru mode
493
+ if (header.connection.toLowerCase() == 'upgrade') {
494
+ // Websocket upgrade succesful
495
+ obj.socketParseState = 2;
496
+ } else {
497
+ // Unable to upgrade to web socket
498
+ obj.close();
499
+ }
500
}
501
}
502
webrelayserver.js
+39
-19
@@ -24,6 +24,8 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
24
obj.tlsServer = null;
25
obj.net = require('net');
26
obj.app = obj.express();
27
+ if (args.compression !== false) { obj.app.use(require('compression')()); }
28
+ obj.app.disable('x-powered-by');
29
obj.webRelayServer = null;
30
obj.port = 0;
31
obj.cleanupTimer = null;
@@ -111,17 +113,18 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
113
req.clientIp = ipex;
114
}
115
114
- // Check if this there is a multi-tunnel for this request
115
- if (req.url.startsWith('/control-redirect.ashx?n=')) {
116
+ // If this is a session start or a websocket, have the application handle this
117
+ if ((req.headers.upgrade == 'websocket') || (req.url.startsWith('/control-redirect.ashx?n='))) {
118
return next();
119
} else {
120
+ // If this is a normal request (GET, POST, etc) handle it here
121
if ((req.session.userid != null) && (req.session.rid != null)) {
122
var relaySession = relaySessions[req.session.userid + '/' + req.session.rid];
123
if (relaySession != null) {
121
- // The multi-tunnel session is valid, use it
124
+ // The web relay session is valid, use it
125
relaySession.handleRequest(req, res);
126
} else {
124
- // No multi-tunnel session with this relay identifier, close the HTTP request.
127
+ // No web relay ession with this relay identifier, close the HTTP request.
128
res.end();
129
}
130
} else {
@@ -131,6 +134,38 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
134
}
135
});
136
137
+ // Start the server, only after users and meshes are loaded from the database.
138
+ if (args.tlsoffload) {
139
+ // Setup the HTTP server without TLS
140
+ obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
141
+ } else {
142
+ // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
143
+ 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 };
144
+ obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
145
+ obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
146
+ obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
147
+ obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
148
+ obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
149
+ obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
150
+ }
151
+
152
+ // Handle incoming web socket calls
153
+ obj.app.ws('/*', function (ws, req) {
154
+ if ((req.session.userid != null) && (req.session.rid != null)) {
155
+ var relaySession = relaySessions[req.session.userid + '/' + req.session.rid];
156
+ if (relaySession != null) {
157
+ // The multi-tunnel session is valid, use it
158
+ relaySession.handleWebSocket(ws, req);
159
+ } else {
160
+ // No multi-tunnel session with this relay identifier, close the websocket.
161
+ ws.close();
162
+ }
163
+ } else {
164
+ // The user is not logged in or does not have a relay identifier, close the websocket.
165
+ ws.close();
166
+ }
167
+ });
168
+
169
// This is the magic URL that will setup the relay session
170
obj.app.get('/control-redirect.ashx', function (req, res) {
171
if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
@@ -183,21 +218,6 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
218
// Redirect to root
219
res.redirect('/');
220
});
186
-
187
- // Start the server, only after users and meshes are loaded from the database.
188
- if (args.tlsoffload) {
189
- // Setup the HTTP server without TLS
190
- obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
191
- } else {
192
- // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
193
- 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 };
194
- obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
195
- obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
196
- obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
197
- obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
198
- obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
199
- obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
200
- }
221
}
222
223
// Check that everything is cleaned up