Merge branch 'master' into commercial

This commit is contained in:
AJ ONeal 2018-06-07 01:39:27 +00:00
commit ab35fdc40e
2 changed files with 196 additions and 197 deletions

View File

@ -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 {

View File

@ -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