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));
|
||||
};
|
||||
state.httpTunnelServer = http.createServer(function (req, res) {
|
||||
//res.setHeader('connection', 'close');
|
||||
res.setHeader('connection', 'close');
|
||||
if (state.extensions.webadmin) {
|
||||
state.extensions.webadmin(state, req, res);
|
||||
} 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() {
|
||||
var result = Object.keys(remotes).map(function (jwtoken) {
|
||||
return remotes[jwtoken].deviceId;
|
||||
}).join(';');
|
||||
return result || socketId;
|
||||
}
|
||||
|
||||
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) {
|
||||
ws.send(Packer.pack(addr, data, service), {binary: true});
|
||||
}
|
||||
PromiseA.resolve().then(function () {
|
||||
var conn = remote.clients[cid];
|
||||
conn.tunnelClosing = true;
|
||||
conn.end();
|
||||
|
||||
function getBrowserConn(cid) {
|
||||
var browserConn;
|
||||
Object.keys(remotes).some(function (jwtoken) {
|
||||
if (remotes[jwtoken].clients[cid]) {
|
||||
browserConn = remotes[jwtoken].clients[cid];
|
||||
return true;
|
||||
}
|
||||
// If no data is buffered for writing then we don't need to wait for it to drain.
|
||||
if (!conn.bufferSize) {
|
||||
return timeoutPromise(500);
|
||||
}
|
||||
// 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.
|
||||
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) {
|
||||
var remote;
|
||||
Object.keys(remotes).some(function (jwtoken) {
|
||||
if (remotes[jwtoken].clients[cid]) {
|
||||
remote = remotes[jwtoken];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!remote) {
|
||||
return;
|
||||
function onAuth(token) {
|
||||
var err;
|
||||
if (!token) {
|
||||
err = new Error("invalid access token");
|
||||
err.code = "E_INVALID_TOKEN";
|
||||
return state.Promise.reject(err);
|
||||
}
|
||||
|
||||
PromiseA.resolve().then(function () {
|
||||
var conn = remote.clients[cid];
|
||||
conn.tunnelClosing = true;
|
||||
conn.end();
|
||||
if (!Array.isArray(token.domains)) {
|
||||
if ('string' === typeof token.name) {
|
||||
token.domains = [ token.name ];
|
||||
}
|
||||
}
|
||||
|
||||
// If no data is buffered for writing then we don't need to wait for it to drain.
|
||||
if (!conn.bufferSize) {
|
||||
return timeoutPromise(500);
|
||||
if (!Array.isArray(token.domains) || !token.domains.length) {
|
||||
err = new Error("invalid domains array");
|
||||
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.
|
||||
return new PromiseA(function (resolve) {
|
||||
var timeoutId = setTimeout(resolve, 60*1000);
|
||||
conn.once('drain', function () {
|
||||
clearTimeout(timeoutId);
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
token.pausedConns.forEach(function (conn) {
|
||||
if (!conn.manualPause) {
|
||||
// console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
|
||||
conn.resume();
|
||||
}
|
||||
});
|
||||
}).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);
|
||||
token.pausedConns.length = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function addToken(jwtoken) {
|
||||
token.domains.forEach(function (domainname) {
|
||||
Devices.add(state.deviceLists, domainname, token);
|
||||
});
|
||||
|
||||
function onAuth(token) {
|
||||
var err;
|
||||
if (!token) {
|
||||
err = new Error("invalid access token");
|
||||
err.code = "E_INVALID_TOKEN";
|
||||
return state.Promise.reject(err);
|
||||
}
|
||||
console.log('[DEBUG] got to firstToken check');
|
||||
|
||||
if (!Array.isArray(token.domains)) {
|
||||
if ('string' === typeof token.name) {
|
||||
token.domains = [ token.name ];
|
||||
}
|
||||
}
|
||||
if (!firstToken || firstToken === jwtoken) {
|
||||
firstToken = jwtoken;
|
||||
token.dynamicPorts = [];
|
||||
token.dynamicNames = [];
|
||||
|
||||
if (!Array.isArray(token.domains) || !token.domains.length) {
|
||||
err = new Error("invalid domains array");
|
||||
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;
|
||||
}
|
||||
|
||||
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 ]
|
||||
]
|
||||
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'
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
]
|
||||
, 'control'
|
||||
);
|
||||
}
|
||||
|
||||
remotes[jwtoken] = token;
|
||||
console.info("[ws] authorized", socketId, "for", token.deviceId);
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
remotes[jwtoken] = token;
|
||||
console.info("[ws] authorized", socketId, "for", token.deviceId);
|
||||
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 = {
|
||||
add_token: addToken
|
||||
, auth: addToken
|
||||
|
|
Loading…
Reference in New Issue