begin refactor TelebitRemote

This commit is contained in:
AJ ONeal 2018-09-03 22:56:52 -06:00
parent 4870cd1ee0
commit 6cd2d0ac16
2 changed files with 243 additions and 176 deletions

View File

@ -75,7 +75,7 @@ try {
// ignore // ignore
} }
var controlServer; var controlServer;
var tun; var myRemote;
var controllers = {}; var controllers = {};
function saveConfig(cb) { function saveConfig(cb) {
@ -352,22 +352,21 @@ function serveControlsHelper() {
res.end(JSON.stringify(dumpy)); res.end(JSON.stringify(dumpy));
} }
if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) { function getConfigOnly() {
var resp = JSON.parse(JSON.stringify(state.config)); var resp = JSON.parse(JSON.stringify(state.config));
resp.version = pkg.version; resp.version = pkg.version;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(resp)); res.end(JSON.stringify(resp));
return;
} }
// //
// without proper config // without proper config
// //
function saveAndReport(err, _tun) { function saveAndReport(err/*, _tun*/) {
console.log('[DEBUG] saveAndReport config write', confpath); console.log('[DEBUG] saveAndReport config write', confpath);
console.log(YAML.safeDump(snakeCopy(state.config))); console.log(YAML.safeDump(snakeCopy(state.config)));
if (err) { throw err; } if (err) { throw err; }
tun = _tun; //myRemote = _tun;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) { if (err) {
res.statusCode = 500; res.statusCode = 500;
@ -380,7 +379,8 @@ function serveControlsHelper() {
listSuccess(); listSuccess();
}); });
} }
if (/\b(init|config)\b/.test(opts.pathname)) {
function initOrConfig() {
var conf = {}; var conf = {};
if (!opts.body) { if (!opts.body) {
res.statusCode = 422; res.statusCode = 422;
@ -474,28 +474,31 @@ function serveControlsHelper() {
return; return;
} }
if (tun) { if (!myRemote) {
console.log('ending existing tunnel, starting anew');
tun.end(function () {
console.log('success ending');
rawTunnel(saveAndReport);
});
tun = null;
setTimeout(function () {
if (!tun) {
console.log('failed to end, but starting anyway');
rawTunnel(saveAndReport);
}
}, 3000);
} else {
console.log('no tunnel, starting anew'); console.log('no tunnel, starting anew');
rawTunnel(saveAndReport); if (!state.config.disable) {
startTelebitRemote(saveAndReport);
}
return;
} }
return;
console.log('ending existing tunnel, starting anew');
myRemote.end();
myRemote.once('end', function () {
console.log('success ending');
startTelebitRemote(saveAndReport);
});
myRemote = null;
setTimeout(function () {
if (!myRemote) {
console.log('failed to end, but starting anyway');
startTelebitRemote(saveAndReport);
}
}, 3000);
} }
if (/restart/.test(opts.pathname)) { function restart() {
tun.end(); if (myRemote) { myRemote.end(); }
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true })); res.end(JSON.stringify({ success: true }));
controlServer.close(function () { controlServer.close(function () {
@ -505,35 +508,17 @@ function serveControlsHelper() {
process.exit(22); // use non-success exit code process.exit(22); // use non-success exit code
}); });
}); });
return;
} }
// function invalidConfig() {
// Check for proper config
//
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
res.statusCode = 400; res.statusCode = 400;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { code: "E_CONFIG", message: "Invalid config file. Please run 'telebit init'" } error: { code: "E_CONFIG", message: "Invalid config file. Please run 'telebit init'" }
})); }));
return;
} }
// function saveAndCommit() {
// With proper config
//
if (/http/.test(opts.pathname)) {
controllers.http(req, res, opts);
return;
}
if (/tcp/.test(opts.pathname)) {
controllers.tcp(req, res, opts);
return;
}
if (/save|commit/.test(opts.pathname)) {
state.config.servernames = state.servernames; state.config.servernames = state.servernames;
state.config.ports = state.ports; state.config.ports = state.ports;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
@ -547,23 +532,17 @@ function serveControlsHelper() {
} }
listSuccess(); listSuccess();
}); });
return;
} }
if (/ssh/.test(opts.pathname)) { function enable() {
controllers.ssh(req, res, opts);
return;
}
if (/enable/.test(opts.pathname)) {
delete state.config.disable;// = undefined; delete state.config.disable;// = undefined;
if (tun) { if (myRemote) {
listSuccess(); listSuccess();
return; return;
} }
rawTunnel(function (err, _tun) { startTelebitRemote(function (err/*, _tun*/) {
if (err) { throw err; } if (err) { throw err; }
tun = _tun; //myRemote = _tun;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) { if (err) {
res.statusCode = 500; res.statusCode = 500;
@ -576,12 +555,11 @@ function serveControlsHelper() {
listSuccess(); listSuccess();
}); });
}); });
return;
} }
if (/disable/.test(opts.pathname)) { function disable() {
state.config.disable = true; state.config.disable = true;
if (tun) { tun.end(); tun = null; } if (myRemote) { myRemote.end(); myRemote = null; }
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
if (err) { if (err) {
@ -593,23 +571,71 @@ function serveControlsHelper() {
} }
res.end('{"success":true}'); res.end('{"success":true}');
}); });
return;
} }
if (/status/.test(opts.pathname)) { function getStatus() {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify( res.end(JSON.stringify(
{ status: (state.config.disable ? 'disabled' : 'enabled') { status: (state.config.disable ? 'disabled' : 'enabled')
, ready: ((state.config.relay && (state.config.token || state.config.agreeTos)) ? true : false) , ready: ((state.config.relay && (state.config.token || state.config.agreeTos)) ? true : false)
, active: !!tun , active: !!myRemote
, connected: 'maybe (todo)' , connected: 'maybe (todo)'
, version: pkg.version , version: pkg.version
, servernames: state.servernames , servernames: state.servernames
} }
)); ));
return;
} }
if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) {
getConfigOnly();
return;
}
if (/\b(init|config)\b/.test(opts.pathname)) {
initOrConfig();
return;
}
if (/restart/.test(opts.pathname)) {
restart();
return;
}
//
// Check for proper config
//
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
invalidConfig();
return;
}
//
// With proper config
//
if (/http/.test(opts.pathname)) {
controllers.http(req, res, opts);
return;
}
if (/tcp/.test(opts.pathname)) {
controllers.tcp(req, res, opts);
return;
}
if (/save|commit/.test(opts.pathname)) {
saveAndCommit();
return;
}
if (/ssh/.test(opts.pathname)) {
controllers.ssh(req, res, opts);
return;
}
if (/enable/.test(opts.pathname)) {
enable();
return;
}
if (/disable/.test(opts.pathname)) {
disable();
return;
}
if (/status/.test(opts.pathname)) {
getStatus();
return;
}
if (/list/.test(opts.pathname)) { if (/list/.test(opts.pathname)) {
listSuccess(); listSuccess();
return; return;
@ -618,7 +644,9 @@ function serveControlsHelper() {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}})); res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}}));
}); });
if (fs.existsSync(state._ipc.path)) { if (fs.existsSync(state._ipc.path)) {
console.log("DEBUG ipc path unlink");
fs.unlinkSync(state._ipc.path); fs.unlinkSync(state._ipc.path);
} }
// mask is so that processes owned by other users // mask is so that processes owned by other users
@ -630,15 +658,18 @@ function serveControlsHelper() {
, exclusive: false , exclusive: false
}; };
if ('socket' === state._ipc.type) { if ('socket' === state._ipc.type) {
console.log("DEBUG ipc path make");
require('mkdirp').sync(path.dirname(state._ipc.path)); require('mkdirp').sync(path.dirname(state._ipc.path));
} }
// https://nodejs.org/api/net.html#net_server_listen_options_callback // https://nodejs.org/api/net.html#net_server_listen_options_callback
// path is ignore if port is defined // path is ignore if port is defined
// https://git.coolaj86.com/coolaj86/telebit.js/issues/23#issuecomment-326 // https://git.coolaj86.com/coolaj86/telebit.js/issues/23#issuecomment-326
if (state._ipc.port) { if (state._ipc.port) {
console.log("DEBUG ipc localhost");
serverOpts.host = 'localhost'; serverOpts.host = 'localhost';
serverOpts.port = state._ipc.port; serverOpts.port = state._ipc.port;
} else { } else {
console.log("DEBUG ipc socket path");
serverOpts.path = state._ipc.path; serverOpts.path = state._ipc.path;
} }
controlServer.listen(serverOpts, function () { controlServer.listen(serverOpts, function () {
@ -655,25 +686,26 @@ function serveControlsHelper() {
function serveControls() { function serveControls() {
if (state.config.disable) { if (state.config.disable) {
console.info("[info] starting disabled"); console.info("[info] starting disabled");
serveControlsHelper();
return; return;
} }
if (state.config.relay && (state.config.token || state.config.pretoken)) { if (!(state.config.relay && (state.config.token || state.config.pretoken))) {
console.info("[info] connecting with stored token");
rawTunnel(function (err, _tun) {
if (err) { throw err; }
if (_tun) { tun = _tun; }
setTimeout(function () {
// TODO attach handler to tunnel
serveControlsHelper();
}, 150);
});
return;
} else {
console.info("[info] waiting for init/authentication (missing relay and/or token)"); console.info("[info] waiting for init/authentication (missing relay and/or token)");
serveControlsHelper();
return;
} }
serveControlsHelper(); console.info("[info] connecting with stored token");
startTelebitRemote(function (err/*, _tun*/) {
console.log("DEBUG going to serve controls soon...");
if (err) { throw err; }
//if (_tun) { myRemote = _tun; }
setTimeout(function () {
// TODO attach handler to tunnel
serveControlsHelper();
}, 150);
});
} }
function parseConfig(err, text) { function parseConfig(err, text) {
@ -735,7 +767,7 @@ function parseConfig(err, text) {
} }
} }
function rawTunnel(rawCb) { function startTelebitRemote(rawCb) {
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) { if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
rawCb(null, null); rawCb(null, null);
return; return;
@ -752,11 +784,12 @@ function rawTunnel(rawCb) {
return; return;
} }
if (tun) { if (myRemote) {
rawCb(null, tun); rawCb(null, myRemote);
return; return;
} }
// get the wss url
common.api.wss(state, function (err, wss) { common.api.wss(state, function (err, wss) {
if (err) { rawCb(err); return; } if (err) { rawCb(err); return; }
state.wss = wss; state.wss = wss;
@ -771,7 +804,7 @@ function rawTunnel(rawCb) {
// TODO sortingHat.print(); ? // TODO sortingHat.print(); ?
// TODO Check undefined vs false for greenlock config // TODO Check undefined vs false for greenlock config
var remote = require('../'); var TelebitRemote = require('../').TelebitRemote;
state.greenlockConfig = { state.greenlockConfig = {
version: state.greenlockConf.version || 'draft-11' version: state.greenlockConf.version || 'draft-11'
@ -808,7 +841,7 @@ function rawTunnel(rawCb) {
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig } // { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
console.log("[DEBUG] token", typeof token, token); console.log("[DEBUG] token", typeof token, token);
tun = remote.connect({ myRemote = TelebitRemote.createConnection({
relay: state.relay relay: state.relay
, wss: state.wss , wss: state.wss
, config: state.config , config: state.config
@ -821,9 +854,20 @@ function rawTunnel(rawCb) {
, ports: state.ports , ports: state.ports
, handlers: state.handlers , handlers: state.handlers
, greenlockConfig: state.greenlockConfig , greenlockConfig: state.greenlockConfig
}, function () {
rawCb(null, myRemote);
});
myRemote.once('error', function (err) {
// Likely causes:
// * DNS lookup failed (no Internet)
// * Rejected (bad authn)
if ('function' === typeof rawCb) {
rawCb(err);
} else {
console.error('Unhandled TelebitRemote Error:');
console.error(err);
}
}); });
rawCb(null, tun);
}); });
} }
@ -891,8 +935,8 @@ function sigHandler() {
// that prevents us from exitting, in which case we want the user to be able to send // that prevents us from exitting, in which case we want the user to be able to send
// the signal again and exit the way it normally would. // the signal again and exit the way it normally would.
process.removeListener('SIGINT', sigHandler); process.removeListener('SIGINT', sigHandler);
if (tun) { if (myRemote) {
tun.end(); myRemote.end();
} }
if (controlServer) { if (controlServer) {
controlServer.close(); controlServer.close();
@ -914,6 +958,7 @@ state.net = state.net || {
} }
}; };
console.log('DEBUG parse config');
fs.readFile(confpath, 'utf8', parseConfig); fs.readFile(confpath, 'utf8', parseConfig);
}()); }());

View File

@ -11,6 +11,7 @@ var WebSocket = require('ws');
var sni = require('sni'); var sni = require('sni');
var Packer = require('proxy-packer'); var Packer = require('proxy-packer');
var os = require('os'); var os = require('os');
var EventEmitter = require('events').EventEmitter;
function timeoutPromise(duration) { function timeoutPromise(duration) {
return new PromiseA(function (resolve) { return new PromiseA(function (resolve) {
@ -18,8 +19,15 @@ function timeoutPromise(duration) {
}); });
} }
function _connect(state) { function TelebitRemote(state) {
// jshint latedef:false // jshint latedef:false
if (!(this instanceof TelebitRemote)) {
return new TelebitRemote(state);
}
EventEmitter.call(this);
var me = this;
var defaultHttpTimeout = (2 * 60); var defaultHttpTimeout = (2 * 60);
var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000; var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
var pongTimeout = state.pongTimeout || 10*1000; var pongTimeout = state.pongTimeout || 10*1000;
@ -315,7 +323,7 @@ function _connect(state) {
if (clientHandlers.write(cid, tun)) { return; } if (clientHandlers.write(cid, tun)) { return; }
wstunneler.pause(); wstunneler._socket.pause();
require(state.sortingHat).assign(state, tun, function (err, conn) { require(state.sortingHat).assign(state, tun, function (err, conn) {
if (err) { if (err) {
err.message = err.message.replace(/:tun_id/, tun._id); err.message = err.message.replace(/:tun_id/, tun._id);
@ -324,7 +332,7 @@ function _connect(state) {
} }
clientHandlers.add(conn, cid, tun); clientHandlers.add(conn, cid, tun);
if (tun.data) { conn.write(tun.data); } if (tun.data) { conn.write(tun.data); }
wstunneler.resume(); wstunneler._socket.resume();
}); });
} }
@ -512,7 +520,8 @@ function _connect(state) {
} }
}; };
function connect() { var connPromise;
me.connect = function connect() {
if (wstunneler) { if (wstunneler) {
console.warn('attempted to connect with connection already active'); console.warn('attempted to connect with connection already active');
return; return;
@ -540,7 +549,11 @@ function _connect(state) {
console.info("[connect] '" + (state.wss || state.relay) + "'"); console.info("[connect] '" + (state.wss || state.relay) + "'");
var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth; var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure }); wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
wstunneler.on('open', wsHandlers.onOpen); // XXXXXX
wstunneler.on('open', function () {
me.emit('connect');
wsHandlers.onOpen();
});
wstunneler.on('close', wsHandlers.onClose); wstunneler.on('close', wsHandlers.onClose);
wstunneler.on('error', wsHandlers.onError); wstunneler.on('error', wsHandlers.onError);
@ -555,108 +568,117 @@ function _connect(state) {
} }
machine.fns.addChunk(data, flags); machine.fns.addChunk(data, flags);
}); });
} };
connect(); me.end = function() {
tokens.length = 0;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
var connPromise; if (wstunneler) {
return { try {
end: function(cb) { wstunneler.close(1000, 're-connect');
tokens.length = 0; wstunneler.on('close', function () {
me.emit('end');
});
} catch(e) {
console.error("[error] wstunneler.close()");
console.error(e);
}
}
};
me.authz = me.append = function (token) {
if (!token) {
throw new Error("attempted to append empty token");
}
if ('undefined' === token) {
throw new Error("attempted to append token as the string 'undefined'");
}
if (tokens.indexOf(token) >= 0) {
return PromiseA.resolve();
}
tokens.push(token);
var prom;
if (tokens.length === 1 && !wstunneler) {
// We just added the only token in the list, and the websocket connection isn't up
// so we need to restart the connection.
if (timeoutId) { if (timeoutId) {
// Handle the case were the last token was removed and this token added between
// reconnect attempts to make sure we don't try openning multiple connections.
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = null; timeoutId = null;
} }
if (wstunneler) { // We want this case to behave as much like the other case as we can, but we don't have
try { // the same kind of reponses when we open brand new connections, so we have to rely on
wstunneler.close(cb); // the 'hello' and the 'un-associated' error commands to determine if the token is good.
} catch(e) { prom = connPromise = new PromiseA(function (resolve, reject) {
console.error("[error] wstunneler.close()"); connCallback = function (err) {
console.error(e); connCallback = null;
} connPromise = null;
} if (err) {
reject(err);
} else {
resolve();
}
};
});
connect();
} }
, append: function (token) { else if (connPromise) {
if (!token) { prom = connPromise.then(function () {
throw new Error("attempted to append empty token"); return sendCommand('add_token', token);
} });
if ('undefined' === token) { }
throw new Error("attempted to append token as the string 'undefined'"); else {
} prom = sendCommand('add_token', token);
if (tokens.indexOf(token) >= 0) { }
prom.catch(function (err) {
console.error('adding token', token, 'failed:', err);
// Most probably an invalid token of some kind, so we don't really want to keep it.
tokens.splice(tokens.indexOf(token), 1);
});
return prom;
};
me.clear = function (token) {
if (typeof token === 'undefined') {
token = '*';
}
if (token === '*') {
tokens.length = 0;
} else {
var index = tokens.indexOf(token);
if (index < 0) {
return PromiseA.resolve(); return PromiseA.resolve();
} }
tokens.push(token); tokens.splice(index);
var prom;
if (tokens.length === 1 && !wstunneler) {
// We just added the only token in the list, and the websocket connection isn't up
// so we need to restart the connection.
if (timeoutId) {
// Handle the case were the last token was removed and this token added between
// reconnect attempts to make sure we don't try openning multiple connections.
clearTimeout(timeoutId);
timeoutId = null;
}
// We want this case to behave as much like the other case as we can, but we don't have
// the same kind of reponses when we open brand new connections, so we have to rely on
// the 'hello' and the 'un-associated' error commands to determine if the token is good.
prom = connPromise = new PromiseA(function (resolve, reject) {
connCallback = function (err) {
connCallback = null;
connPromise = null;
if (err) {
reject(err);
} else {
resolve();
}
};
});
connect();
}
else if (connPromise) {
prom = connPromise.then(function () {
return sendCommand('add_token', token);
});
}
else {
prom = sendCommand('add_token', token);
}
prom.catch(function (err) {
console.error('adding token', token, 'failed:', err);
// Most probably an invalid token of some kind, so we don't really want to keep it.
tokens.splice(tokens.indexOf(token), 1);
});
return prom;
} }
, clear: function (token) {
if (typeof token === 'undefined') {
token = '*';
}
if (token === '*') { var prom = sendCommand('delete_token', token);
tokens.length = 0; prom.catch(function (err) {
} else { console.error('clearing token', token, 'failed:', err);
var index = tokens.indexOf(token); });
if (index < 0) {
return PromiseA.resolve();
}
tokens.splice(index);
}
var prom = sendCommand('delete_token', token); return prom;
prom.catch(function (err) {
console.error('clearing token', token, 'failed:', err);
});
return prom;
}
}; };
} }
TelebitRemote.prototype = EventEmitter.prototype;
module.exports.connect = _connect; TelebitRemote.create = function (opts) {
module.exports.createConnection = _connect; return new TelebitRemote(opts);
};
TelebitRemote.createConnection = function (opts, cb) {
var tunnel = TelebitRemote.create(opts);
tunnel.connect(opts);
tunnel.once('connect', cb);
return tunnel;
};
TelebitRemote.connect = TelebitRemote.createConnection;
module.exports.TelebitRemote = TelebitRemote;
}()); }());