Web relay improvements, #4172
Ylian Saint-Hilaire committed
Jun 25, 2022 at 16:22 UTC
3b93d1adf7a591f9b8352e800c7ee6a3d8b0cca9
2 files changed
+70
-33
apprelays.js
+37
-24
@@ -63,20 +63,27 @@ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
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.parent = parent;
67
obj.lastOperation = Date.now();
68
+ obj.domain = domain;
69
obj.userid = userid;
70
+ obj.nodeid = nodeid;
71
+ obj.addr = addr;
72
+ obj.port = port;
73
var pendingRequests = [];
69
- var activeRequests = 0;
74
var nextTunnelId = 1;
75
var tunnels = {};
76
77
+ // Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
78
+ obj.webCookie = null;
79
+
80
// Events
81
obj.closed = false;
82
obj.onclose = null;
83
84
// Handle new HTTP request
85
obj.handleRequest = function (req, res) {
79
- console.log('handleRequest', req.url);
86
+ //console.log('handleRequest', req.url);
87
pendingRequests.push([req, res]);
88
handleNextRequest();
89
}
@@ -89,7 +96,7 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
96
count += (tunnels[i].isWebSocket ? 0 : 1);
97
if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) {
98
// Found a free tunnel, use it
92
- console.log('handleNextRequest-found empty tunnel');
99
+ //console.log('handleNextRequest-found empty tunnel');
100
const x = pendingRequests.shift();
101
tunnels[i].processRequest(x[0], x[1]);
102
return;
@@ -99,11 +106,11 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
106
if (count > 0) return;
107
108
// 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]); } }
109
+ //console.log('handleNextRequest-starting new tunnel');
110
+ const tunnel = module.exports.CreateWebRelay(obj, db, args, domain);
111
+ tunnel.onclose = function (tunnelId) { delete tunnels[tunnelId]; }
112
+ tunnel.onconnect = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
113
+ tunnel.oncompleted = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
114
tunnel.connect(userid, nodeid, addr, port);
115
tunnel.tunnelId = nextTunnelId++;
116
tunnels[tunnel.tunnelId] = tunnel;
@@ -144,26 +151,27 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
151
obj.processRequest = function (req, res) {
152
if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
153
147
- console.log('processRequest-start', req.method);
154
+ //console.log('processRequest-start', req.method);
155
156
// Construct the HTTP request
157
var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
158
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
- }
159
+ const blockedHeaders = ['origin', 'host', 'cookie']; // These are headers we do not forward
160
+ for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
161
+ if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
162
request += '\r\n';
163
164
+ //console.log('request', request);
165
+
166
if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) {
167
// Read the HTTP body and send the request to the device
168
obj.requestBinary = [Buffer.from(request)];
169
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'); });
170
+ req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; });
171
} else {
172
// Request has no body, send it now
173
obj.wsClient.send(Buffer.from(request));
166
- console.log('processRequest-sent-nobody');
174
+ //console.log('processRequest-sent-nobody');
175
}
176
obj.res = res;
177
}
@@ -173,6 +181,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
181
if (obj.closed == true) return;
182
obj.closed = true;
183
184
+ /*
185
// Event the session ending
186
if ((obj.startTime) && (obj.meshid != null)) {
187
// Collect how many raw bytes where received and sent.
@@ -187,6 +196,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
196
delete obj.startTime;
197
delete obj.sessionid;
198
}
199
+ */
200
if (obj.wsClient) {
201
obj.wsClient.removeAllListeners('open');
202
obj.wsClient.removeAllListeners('message');
@@ -213,7 +223,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
223
// Encode a cookie for the mesh relay
224
const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port };
225
if (addr != null) { cookieContent.tcpaddr = addr; }
216
- const cookie = parent.encodeCookie(cookieContent, parent.loginCookieEncryptionKey);
226
+ const cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
227
228
try {
229
// Setup the correct URL with domain and use TLS only if needed.
@@ -222,9 +232,9 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
232
var domainadd = '';
233
if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
234
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);
235
+ parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
236
obj.wsClient = new WebSocket(url, options);
227
- obj.wsClient.on('open', function () { parent.debug('relay', 'TCP: Relay websocket open'); });
237
+ obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); });
238
obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
239
if (obj.relayActive == false) {
240
if ((data == 'c') || (data == 'cr')) {
@@ -243,8 +253,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
253
processHttpData(data.toString('binary'));
254
}
255
});
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(); });
256
+ obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
257
+ obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
258
} catch (ex) {
259
console.log(ex);
260
}
@@ -310,10 +320,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
320
321
// This is a fully parsed HTTP response from the remote device
322
function processHttpResponse(header, data) {
313
- console.log('processHttpResponse');
314
-
323
+ //console.log('processHttpResponse', header);
324
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
325
+ const blockHeaders = ['Directive' ]; // These are headers we do not forward
326
+ for (var i in header) {
327
+ if (i == 'set-cookie') { parent.webCookie = header[i]; } // Keep the cookie, don't forward it
328
+ else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked
329
+ }
330
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
331
obj.res.end(data, 'binary'); // Write the data
332
delete obj.res;
@@ -329,7 +342,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
342
return true;
343
}
344
332
- parent.debug('relay', 'TCP: Request for web relay');
345
+ parent.parent.debug('relay', 'TCP: Request for web relay');
346
return obj;
347
};
348
webrelayserver.js
+33
-9
@@ -116,8 +116,15 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
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; }
119
+ if (relayMultiTunnel != null) {
120
+ // The multi-tunnel session is valid, use it
121
+ relayMultiTunnel.handleRequest(req, res);
122
+ } else {
123
+ // No multi-tunnel session with this relay identifier, close the HTTP request.
124
+ res.end();
125
+ }
126
} else {
127
+ // The user is not logged in or does not have a relay identifier, close the HTTP request.
128
res.end();
129
}
130
}
@@ -136,15 +143,32 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
143
const userid = req.session.userid;
144
const domainid = userid.split('/')[1];
145
const domain = parent.config.domains[domainid];
146
+ const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
147
+ const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
148
+ const port = parseInt(req.query.p);
149
+
150
+ // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
151
+ var relayMultiTunnel = null;
152
+ for (var i in relayMultiTunnels) {
153
+ const xrelayMultiTunnel = relayMultiTunnels[i];
154
+ if ((xrelayMultiTunnel.domain.id == domain.id) && (xrelayMultiTunnel.userid == userid) && (xrelayMultiTunnel.nodeid == nodeid) && (xrelayMultiTunnel.addr == addr) && (xrelayMultiTunnel.port == port)) {
155
+ relayMultiTunnel = xrelayMultiTunnel; // We found an exact match
156
+ }
157
+ }
158
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;
159
+ if (relayMultiTunnel != null) {
160
+ // Since we found a match, use it
161
+ req.session.rid = relayMultiTunnel.multiTunnelId;
162
+ } else {
163
+ // Create the multi-tunnel
164
+ relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, nodeid, addr, port);
165
+ relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; }
166
+ relayMultiTunnel.multiTunnelId = nextMultiTunnelId++;
167
+
168
+ // Set the tunnel
169
+ relayMultiTunnels[userid + '/' + relayMultiTunnel.multiTunnelId] = relayMultiTunnel;
170
+ req.session.rid = relayMultiTunnel.multiTunnelId;
171
+ }
172
173
// Redirect to root
174
res.redirect('/');