Merge branch 'master' into commercial
This commit is contained in:
commit
ab35fdc40e
|
@ -120,7 +120,7 @@ module.exports.create = function (state) {
|
||||||
serveAdmin(req, res, finalhandler(req, res));
|
serveAdmin(req, res, finalhandler(req, res));
|
||||||
};
|
};
|
||||||
state.httpTunnelServer = http.createServer(function (req, res) {
|
state.httpTunnelServer = http.createServer(function (req, res) {
|
||||||
//res.setHeader('connection', 'close');
|
res.setHeader('connection', 'close');
|
||||||
if (state.extensions.webadmin) {
|
if (state.extensions.webadmin) {
|
||||||
state.extensions.webadmin(state, req, res);
|
state.extensions.webadmin(state, req, res);
|
||||||
} else {
|
} else {
|
||||||
|
|
391
lib/relay.js
391
lib/relay.js
|
@ -113,228 +113,227 @@ module.exports.create = function (state) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function next() {
|
function logName() {
|
||||||
|
var result = Object.keys(remotes).map(function (jwtoken) {
|
||||||
|
return remotes[jwtoken].deviceId;
|
||||||
|
}).join(';');
|
||||||
|
|
||||||
function logName() {
|
return result || socketId;
|
||||||
var result = Object.keys(remotes).map(function (jwtoken) {
|
}
|
||||||
return remotes[jwtoken].deviceId;
|
|
||||||
}).join(';');
|
|
||||||
|
|
||||||
return result || socketId;
|
function sendTunnelMsg(addr, data, service) {
|
||||||
|
ws.send(Packer.pack(addr, data, service), {binary: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowserConn(cid) {
|
||||||
|
var browserConn;
|
||||||
|
Object.keys(remotes).some(function (jwtoken) {
|
||||||
|
if (remotes[jwtoken].clients[cid]) {
|
||||||
|
browserConn = remotes[jwtoken].clients[cid];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return browserConn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBrowserConn(cid) {
|
||||||
|
var remote;
|
||||||
|
Object.keys(remotes).some(function (jwtoken) {
|
||||||
|
if (remotes[jwtoken].clients[cid]) {
|
||||||
|
remote = remotes[jwtoken];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!remote) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendTunnelMsg(addr, data, service) {
|
PromiseA.resolve().then(function () {
|
||||||
ws.send(Packer.pack(addr, data, service), {binary: true});
|
var conn = remote.clients[cid];
|
||||||
}
|
conn.tunnelClosing = true;
|
||||||
|
conn.end();
|
||||||
|
|
||||||
function getBrowserConn(cid) {
|
// If no data is buffered for writing then we don't need to wait for it to drain.
|
||||||
var browserConn;
|
if (!conn.bufferSize) {
|
||||||
Object.keys(remotes).some(function (jwtoken) {
|
return timeoutPromise(500);
|
||||||
if (remotes[jwtoken].clients[cid]) {
|
}
|
||||||
browserConn = remotes[jwtoken].clients[cid];
|
// Otherwise we want the connection to be able to finish, but we also want to impose
|
||||||
return true;
|
// a time limit for it to drain, since it shouldn't have more than 1MB buffered.
|
||||||
}
|
return new PromiseA(function (resolve) {
|
||||||
|
var timeoutId = setTimeout(resolve, 60*1000);
|
||||||
|
conn.once('drain', function () {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}).then(function () {
|
||||||
|
if (remote.clients[cid]) {
|
||||||
|
console.warn(cid, 'browser connection still present after calling `end`');
|
||||||
|
remote.clients[cid].destroy();
|
||||||
|
return timeoutPromise(500);
|
||||||
|
}
|
||||||
|
}).then(function () {
|
||||||
|
if (remote.clients[cid]) {
|
||||||
|
console.error(cid, 'browser connection still present after calling `destroy`');
|
||||||
|
delete remote.clients[cid];
|
||||||
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.warn('failed to close browser connection', cid, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return browserConn;
|
function addToken(jwtoken) {
|
||||||
}
|
|
||||||
|
|
||||||
function closeBrowserConn(cid) {
|
function onAuth(token) {
|
||||||
var remote;
|
var err;
|
||||||
Object.keys(remotes).some(function (jwtoken) {
|
if (!token) {
|
||||||
if (remotes[jwtoken].clients[cid]) {
|
err = new Error("invalid access token");
|
||||||
remote = remotes[jwtoken];
|
err.code = "E_INVALID_TOKEN";
|
||||||
return true;
|
return state.Promise.reject(err);
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!remote) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PromiseA.resolve().then(function () {
|
if (!Array.isArray(token.domains)) {
|
||||||
var conn = remote.clients[cid];
|
if ('string' === typeof token.name) {
|
||||||
conn.tunnelClosing = true;
|
token.domains = [ token.name ];
|
||||||
conn.end();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If no data is buffered for writing then we don't need to wait for it to drain.
|
if (!Array.isArray(token.domains) || !token.domains.length) {
|
||||||
if (!conn.bufferSize) {
|
err = new Error("invalid domains array");
|
||||||
return timeoutPromise(500);
|
err.code = "E_INVALID_NAME";
|
||||||
|
return state.Promise.reject(err);
|
||||||
|
}
|
||||||
|
if (token.domains.some(function (name) { return typeof name !== 'string'; })) {
|
||||||
|
err = new Error("invalid domain name(s)");
|
||||||
|
err.code = "E_INVALID_NAME";
|
||||||
|
return state.Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the custom properties we need to manage this remote, then add it to all the relevant
|
||||||
|
// domains and the list of all this websocket's remotes.
|
||||||
|
token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
|
||||||
|
token.ws = ws;
|
||||||
|
token.upgradeReq = upgradeReq;
|
||||||
|
token.clients = {};
|
||||||
|
|
||||||
|
token.pausedConns = [];
|
||||||
|
ws._socket.on('drain', function () {
|
||||||
|
// the websocket library has it's own buffer apart from node's socket buffer, but that one
|
||||||
|
// is much more difficult to watch, so we watch for the lower level buffer to drain and
|
||||||
|
// then check to see if the upper level buffer is still too full to write to. Note that
|
||||||
|
// the websocket library buffer has something to do with compression, so I'm not requiring
|
||||||
|
// that to be 0 before we start up again.
|
||||||
|
if (ws.bufferedAmount > 128*1024) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Otherwise we want the connection to be able to finish, but we also want to impose
|
|
||||||
// a time limit for it to drain, since it shouldn't have more than 1MB buffered.
|
token.pausedConns.forEach(function (conn) {
|
||||||
return new PromiseA(function (resolve) {
|
if (!conn.manualPause) {
|
||||||
var timeoutId = setTimeout(resolve, 60*1000);
|
// console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
|
||||||
conn.once('drain', function () {
|
conn.resume();
|
||||||
clearTimeout(timeoutId);
|
}
|
||||||
setTimeout(resolve, 500);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}).then(function () {
|
token.pausedConns.length = 0;
|
||||||
if (remote.clients[cid]) {
|
|
||||||
console.warn(cid, 'browser connection still present after calling `end`');
|
|
||||||
remote.clients[cid].destroy();
|
|
||||||
return timeoutPromise(500);
|
|
||||||
}
|
|
||||||
}).then(function () {
|
|
||||||
if (remote.clients[cid]) {
|
|
||||||
console.error(cid, 'browser connection still present after calling `destroy`');
|
|
||||||
delete remote.clients[cid];
|
|
||||||
}
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.warn('failed to close browser connection', cid, err);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function addToken(jwtoken) {
|
token.domains.forEach(function (domainname) {
|
||||||
|
Devices.add(state.deviceLists, domainname, token);
|
||||||
|
});
|
||||||
|
|
||||||
function onAuth(token) {
|
console.log('[DEBUG] got to firstToken check');
|
||||||
var err;
|
|
||||||
if (!token) {
|
|
||||||
err = new Error("invalid access token");
|
|
||||||
err.code = "E_INVALID_TOKEN";
|
|
||||||
return state.Promise.reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(token.domains)) {
|
if (!firstToken || firstToken === jwtoken) {
|
||||||
if ('string' === typeof token.name) {
|
firstToken = jwtoken;
|
||||||
token.domains = [ token.name ];
|
token.dynamicPorts = [];
|
||||||
}
|
token.dynamicNames = [];
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(token.domains) || !token.domains.length) {
|
function onDynTcpReady() {
|
||||||
err = new Error("invalid domains array");
|
var serviceport = this.address().port;
|
||||||
err.code = "E_INVALID_NAME";
|
console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId);
|
||||||
return state.Promise.reject(err);
|
token.dynamicPorts.push(serviceport);
|
||||||
}
|
Devices.add(state.deviceLists, serviceport, token);
|
||||||
if (token.domains.some(function (name) { return typeof name !== 'string'; })) {
|
var hri = require('human-readable-ids').hri;
|
||||||
err = new Error("invalid domain name(s)");
|
var hrname = hri.random() + '.telebit.cloud';
|
||||||
err.code = "E_INVALID_NAME";
|
token.dynamicNames.push(hrname);
|
||||||
return state.Promise.reject(err);
|
// TODO restrict to authenticated device
|
||||||
}
|
// TODO pull servername from config
|
||||||
|
// TODO remove hrname on disconnect
|
||||||
// Add the custom properties we need to manage this remote, then add it to all the relevant
|
Devices.add(state.deviceLists, hrname, token);
|
||||||
// domains and the list of all this websocket's remotes.
|
sendTunnelMsg(
|
||||||
token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
|
null
|
||||||
token.ws = ws;
|
, [ 2
|
||||||
token.upgradeReq = upgradeReq;
|
, 'grant'
|
||||||
token.clients = {};
|
, [ ['ssh+https', hrname, 443 ]
|
||||||
|
, ['ssh', 'ssh.telebit.cloud', serviceport ]
|
||||||
token.pausedConns = [];
|
, ['tcp', 'tcp.telebit.cloud', serviceport]
|
||||||
ws._socket.on('drain', function () {
|
, ['https', hrname ]
|
||||||
// the websocket library has it's own buffer apart from node's socket buffer, but that one
|
|
||||||
// is much more difficult to watch, so we watch for the lower level buffer to drain and
|
|
||||||
// then check to see if the upper level buffer is still too full to write to. Note that
|
|
||||||
// the websocket library buffer has something to do with compression, so I'm not requiring
|
|
||||||
// that to be 0 before we start up again.
|
|
||||||
if (ws.bufferedAmount > 128*1024) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
token.pausedConns.forEach(function (conn) {
|
|
||||||
if (!conn.manualPause) {
|
|
||||||
// console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
|
|
||||||
conn.resume();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
token.pausedConns.length = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
token.domains.forEach(function (domainname) {
|
|
||||||
Devices.add(state.deviceLists, domainname, token);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[DEBUG] got to firstToken check');
|
|
||||||
|
|
||||||
if (!firstToken || firstToken === jwtoken) {
|
|
||||||
firstToken = jwtoken;
|
|
||||||
token.dynamicPorts = [];
|
|
||||||
token.dynamicNames = [];
|
|
||||||
|
|
||||||
function onDynTcpReady() {
|
|
||||||
var serviceport = this.address().port;
|
|
||||||
console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId);
|
|
||||||
token.dynamicPorts.push(serviceport);
|
|
||||||
Devices.add(state.deviceLists, serviceport, token);
|
|
||||||
var hri = require('human-readable-ids').hri;
|
|
||||||
var hrname = hri.random() + '.telebit.cloud';
|
|
||||||
token.dynamicNames.push(hrname);
|
|
||||||
// TODO restrict to authenticated device
|
|
||||||
// TODO pull servername from config
|
|
||||||
// TODO remove hrname on disconnect
|
|
||||||
Devices.add(state.deviceLists, hrname, token);
|
|
||||||
sendTunnelMsg(
|
|
||||||
null
|
|
||||||
, [ 2
|
|
||||||
, 'grant'
|
|
||||||
, [ ['ssh+https', hrname, 443 ]
|
|
||||||
, ['ssh', 'ssh.telebit.cloud', serviceport ]
|
|
||||||
, ['tcp', 'tcp.telebit.cloud', serviceport]
|
|
||||||
, ['https', hrname ]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
, 'control'
|
]
|
||||||
);
|
, 'control'
|
||||||
}
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
token.server = require('net').createServer(onDynTcpConn).listen(0, onDynTcpReady);
|
|
||||||
token.server.on('error', function (e) {
|
|
||||||
console.error("Server Error assigning a dynamic port to a new connection:", e);
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
// what a wonderful problem it will be the day that this bug needs to be fixed
|
|
||||||
// (i.e. there are enough users to run out of ports)
|
|
||||||
console.error("Error assigning a dynamic port to a new connection:", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remotes[jwtoken] = token;
|
try {
|
||||||
console.info("[ws] authorized", socketId, "for", token.deviceId);
|
token.server = require('net').createServer(onDynTcpConn).listen(0, onDynTcpReady);
|
||||||
return null;
|
token.server.on('error', function (e) {
|
||||||
|
console.error("Server Error assigning a dynamic port to a new connection:", e);
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
// what a wonderful problem it will be the day that this bug needs to be fixed
|
||||||
|
// (i.e. there are enough users to run out of ports)
|
||||||
|
console.error("Error assigning a dynamic port to a new connection:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remotes[jwtoken]) {
|
remotes[jwtoken] = token;
|
||||||
// return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
|
console.info("[ws] authorized", socketId, "for", token.deviceId);
|
||||||
return state.Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.authenticate({ auth: jwtoken }).then(onAuth);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeToken(jwtoken) {
|
|
||||||
var remote = remotes[jwtoken];
|
|
||||||
if (!remote) {
|
|
||||||
return { message: 'specified token not present', code: 'E_INVALID_TOKEN'};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent any more browser connections being sent to this remote, and any existing
|
|
||||||
// connections from trying to send more data across the connection.
|
|
||||||
remote.domains.forEach(function (domainname) {
|
|
||||||
Devices.remove(state.deviceLists, domainname, remote);
|
|
||||||
});
|
|
||||||
remote.dynamicPorts.forEach(function (portnumber) {
|
|
||||||
Devices.remove(state.deviceLists, portnumber, remote);
|
|
||||||
});
|
|
||||||
remote.ws = null;
|
|
||||||
remote.upgradeReq = null;
|
|
||||||
if (remote.server) {
|
|
||||||
remote.serverPort = remote.server.address().port;
|
|
||||||
remote.server.close(function () {
|
|
||||||
console.log("[DynTcpConn] closing server for ", remote.serverPort);
|
|
||||||
remote.serverPort = null;
|
|
||||||
});
|
|
||||||
remote.server = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all of the existing browser connections associated with this websocket connection.
|
|
||||||
Object.keys(remote.clients).forEach(function (cid) {
|
|
||||||
closeBrowserConn(cid);
|
|
||||||
});
|
|
||||||
delete remotes[jwtoken];
|
|
||||||
console.log("[ws] removed token '" + remote.deviceId + "' from", socketId);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remotes[jwtoken]) {
|
||||||
|
// return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
|
||||||
|
return state.Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.authenticate({ auth: jwtoken }).then(onAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeToken(jwtoken) {
|
||||||
|
var remote = remotes[jwtoken];
|
||||||
|
if (!remote) {
|
||||||
|
return { message: 'specified token not present', code: 'E_INVALID_TOKEN'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent any more browser connections being sent to this remote, and any existing
|
||||||
|
// connections from trying to send more data across the connection.
|
||||||
|
remote.domains.forEach(function (domainname) {
|
||||||
|
Devices.remove(state.deviceLists, domainname, remote);
|
||||||
|
});
|
||||||
|
remote.dynamicPorts.forEach(function (portnumber) {
|
||||||
|
Devices.remove(state.deviceLists, portnumber, remote);
|
||||||
|
});
|
||||||
|
remote.ws = null;
|
||||||
|
remote.upgradeReq = null;
|
||||||
|
if (remote.server) {
|
||||||
|
remote.serverPort = remote.server.address().port;
|
||||||
|
remote.server.close(function () {
|
||||||
|
console.log("[DynTcpConn] closing server for ", remote.serverPort);
|
||||||
|
remote.serverPort = null;
|
||||||
|
});
|
||||||
|
remote.server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all of the existing browser connections associated with this websocket connection.
|
||||||
|
Object.keys(remote.clients).forEach(function (cid) {
|
||||||
|
closeBrowserConn(cid);
|
||||||
|
});
|
||||||
|
delete remotes[jwtoken];
|
||||||
|
console.log("[ws] removed token '" + remote.deviceId + "' from", socketId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
var commandHandlers = {
|
var commandHandlers = {
|
||||||
add_token: addToken
|
add_token: addToken
|
||||||
, auth: addToken
|
, auth: addToken
|
||||||
|
|
Loading…
Reference in New Issue