WIP promisify (bug: enable acts as toggle)

This commit is contained in:
AJ ONeal 2018-09-06 03:11:26 -06:00
parent 32f969cb18
commit 53ee77d8d3
2 changed files with 256 additions and 163 deletions

View File

@ -2,6 +2,13 @@
(function () { (function () {
'use strict'; 'use strict';
var PromiseA;
try {
PromiseA = require('bluebird');
} catch(e) {
PromiseA = global.Promise;
}
var pkg = require('../package.json'); var pkg = require('../package.json');
var url = require('url'); var url = require('url');
@ -16,7 +23,7 @@ var camelCopy = recase.camelCopy.bind(recase);
var snakeCopy = recase.snakeCopy.bind(recase); var snakeCopy = recase.snakeCopy.bind(recase);
var TelebitRemote = require('../').TelebitRemote; var TelebitRemote = require('../').TelebitRemote;
var state = { homedir: os.homedir(), servernames: {}, ports: {} }; var state = { homedir: os.homedir(), servernames: {}, ports: {}, keepAlive: true };
var argv = process.argv.slice(2); var argv = process.argv.slice(2);
@ -68,7 +75,9 @@ if (!confpath || /^--/.test(confpath)) {
help(); help();
process.exit(1); process.exit(1);
} }
var tokenpath = path.join(path.dirname(confpath), 'access_token.txt');
state._confpath = confpath;
var tokenpath = path.join(path.dirname(state._confpath), 'access_token.txt');
var token; var token;
try { try {
token = fs.readFileSync(tokenpath, 'ascii').trim(); token = fs.readFileSync(tokenpath, 'ascii').trim();
@ -79,10 +88,6 @@ try {
var controlServer; var controlServer;
var myRemote; var myRemote;
var controllers = {};
function saveConfig(cb) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb);
}
function getServername(servernames, sub) { function getServername(servernames, sub) {
if (state.servernames[sub]) { if (state.servernames[sub]) {
return sub; return sub;
@ -107,6 +112,11 @@ function getServername(servernames, sub) {
} }
})[0]; })[0];
} }
function saveConfig(cb) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb);
}
var controllers = {};
controllers.http = function (req, res, opts) { controllers.http = function (req, res, opts) {
function getAppname(pathname) { function getAppname(pathname) {
// port number // port number
@ -364,11 +374,9 @@ function serveControlsHelper() {
// //
// without proper config // without proper config
// //
function saveAndReport(err/*, _tun*/) { function saveAndReport() {
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; }
//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;
@ -476,39 +484,30 @@ function serveControlsHelper() {
return; return;
} }
if (!myRemote) { // init also means enable
console.log('no tunnel, starting anew'); delete state.config.disable;
if (!state.config.disable) { safeStartTelebitRemote(true).then(saveAndReport).catch(handleError);
startTelebitRemote(saveAndReport);
}
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);
} }
function restart() { function restart() {
// failsafe
setTimeout(function () {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
setTimeout(function () {
process.exit(33);
}, 500);
}, 5 * 1000);
if (myRemote) { myRemote.end(); } if (myRemote) { myRemote.end(); }
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
controlServer.close(function () { controlServer.close(function () {
// TODO closeAll other things res.setHeader('Content-Type', 'application/json');
process.nextTick(function () { res.end(JSON.stringify({ success: true }));
setTimeout(function () {
// system daemon will restart the process // system daemon will restart the process
process.exit(22); // use non-success exit code process.exit(22); // use non-success exit code
}); }, 500);
}); });
} }
@ -536,39 +535,43 @@ function serveControlsHelper() {
}); });
} }
function handleError(err) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: { message: err.message, code: err.code }
}));
}
function enable() { function enable() {
delete state.config.disable;// = undefined; delete state.config.disable;// = undefined;
state.keepAlive = true;
// TODO XXX myRemote.active
if (myRemote) { if (myRemote) {
listSuccess(); listSuccess();
return; return;
} }
startTelebitRemote(function (err/*, _tun*/) { fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) { throw err; } if (err) {
//myRemote = _tun; err.message = "Could not save config file. Perhaps you're user doesn't have permission?";
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { handleError(err);
if (err) { return;
res.statusCode = 500; }
res.setHeader('Content-Type', 'application/json'); safeStartTelebitRemote(true).then(listSuccess).catch(handleError);
res.end(JSON.stringify({
error: { message: "Could not save config file. Perhaps you're user doesn't have permission?" }
}));
return;
}
listSuccess();
});
}); });
} }
function disable() { function disable() {
state.config.disable = true; state.config.disable = true;
state.keepAlive = false;
if (myRemote) { myRemote.end(); myRemote = 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) {
res.statusCode = 500; err.message = "Could not save config file. Perhaps you're user doesn't have permission?";
res.end(JSON.stringify({ handleError(err);
"error":{"message":"Could not save config file. Perhaps you're not running as root?"}
}));
return; return;
} }
res.end('{"success":true}'); res.end('{"success":true}');
@ -689,18 +692,17 @@ function serveControls() {
return; return;
} }
// This will remain in a disconnect state and wait for an init
if (!(state.config.relay && (state.config.token || state.config.pretoken))) { if (!(state.config.relay && (state.config.token || state.config.pretoken))) {
console.info("[info] waiting for init/authentication (missing relay and/or token)"); console.info("[info] waiting for init/authentication (missing relay and/or token)");
return; return;
} }
console.info("[info] connecting with stored token"); console.info("[info] connecting with stored token");
function tryAgain() { state.keepAlive = true;
startTelebitRemote(function (err) { return safeStartTelebitRemote().catch(function (/*err*/) {
if (err) { console.error('error starting (probably internet)', err); setTimeout(tryAgain, 5 * 1000); } // ignore, it'll keep looping anyway
}); });
}
tryAgain();
} }
function parseConfig(err, text) { function parseConfig(err, text) {
@ -777,7 +779,7 @@ function approveDomains(opts, certs, cb) {
cb(new Error("servername not found in allowed list")); cb(new Error("servername not found in allowed list"));
} }
function greenlockHelper() { function greenlockHelper(state) {
// TODO Check undefined vs false for greenlock config // TODO Check undefined vs false for greenlock config
state.greenlockConf = state.config.greenlock || {}; state.greenlockConf = state.config.greenlock || {};
state.greenlockConfig = { state.greenlockConfig = {
@ -794,131 +796,219 @@ function greenlockHelper() {
state.insecure = state.config.relay_ignore_invalid_certificates; state.insecure = state.config.relay_ignore_invalid_certificates;
} }
function startTelebitRemote(rawCb) { function promiseTimeout(t) {
console.log('DEBUG startTelebitRemote'); return new PromiseA(function (resolve) {
setTimeout(resolve, t);
});
}
function startHelper() { var promiseWss = PromiseA.promisify(function (state, fn) {
console.log('DEBUG startHelper'); return common.api.wss(state, fn);
greenlockHelper(); });
// Saves the token
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
function onError(err) { var trPromise;
myRemote = null; function safeStartTelebitRemote() {
console.log('DEBUG err', err); state.keepAlive = false;
// Likely causes: if (trPromise) {
// * DNS lookup failed (no Internet) return trPromise;
// * Rejected (bad authn)
if ('ENOTFOUND' === err.code) {
// DNS issue, probably network is disconnected
setTimeout(function () {
startTelebitRemote(rawCb);
}, 10 * 1000);
return;
}
if ('function' === typeof rawCb) {
rawCb(err);
} else {
console.error('Unhandled TelebitRemote Error:');
console.error(err);
}
}
console.log("[DEBUG] token", typeof token, token);
//state.sortingHat = state.config.sortingHat;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
myRemote = TelebitRemote.createConnection({
relay: state.relay
, wss: state.wss
, config: state.config
, otp: state.otp
, sortingHat: state.config.sortingHat
, net: state.net
, insecure: state.insecure
, token: state.token || state.pretoken // instance
, servernames: state.servernames
, ports: state.ports
, handlers: state.handlers
, greenlockConfig: state.greenlockConfig
}, function () {
console.log('DEBUG on connect');
myRemote.removeListener('error', onError);
myRemote.once('error', retryLoop);
rawCb(null, myRemote);
});
function retryLoop() {
// disconnected somehow
if (myRemote) { myRemote.destroy(); }
myRemote = null;
setTimeout(function () {
startTelebitRemote(function () {});
}, 10 * 1000);
}
myRemote.once('error', onError);
myRemote.once('close', retryLoop);
myRemote.on('grant', state.handlers.grant);
myRemote.on('access_token', state.handlers.access_token);
} }
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) { trPromise = rawStartTelebitRemote();
trPromise.then(function () {
state.keepAlive = true;
trPromise = null;
}).catch(function () {
state.keepAlive = true;
trPromise = rawStartTelebitRemote();
trPromise.then(function () {
state.keepAlive = true;
trPromise = null;
}).catch(function () {
state.keepAlive = true;
console.log('DEBUG state.keepAlive turned off and remote quit');
trPromise = null;
});
});
return trPromise;
}
function rawStartTelebitRemote() {
var err;
var exiting = false;
var localRemote = myRemote;
myRemote = null;
if (localRemote) { console.log('DEBUG destroy() existing'); localRemote.destroy(); }
function safeReload(delay) {
if (exiting) {
// return a junk promise as the prior call
// already passed flow-control to the next promise
// (this is a second or delayed error or close event)
return PromiseA.resolve();
}
exiting = true;
// TODO state.keepAlive?
return promiseTimeout(delay).then(rawStartTelebitRemote);
}
if (state.config.disable) {
console.log('DEBUG disabled or incapable'); console.log('DEBUG disabled or incapable');
rawCb(null, null); err = new Error("connecting is disabled");
return; err.code = 'EDISABLED';
return PromiseA.reject(err);
}
if (!(state.config.token || state.config.agreeTos)) {
console.log('DEBUG Must agreeTos to generate preauth');
err = new Error("Must either supply token (for auth) or agreeTos (for preauth)");
err.code = 'ENOAGREE';
return PromiseA.reject(err);
} }
state.relay = state.config.relay; state.relay = state.config.relay;
if (!state.relay) { if (!state.relay) {
console.log('DEBUG no relay'); console.log('DEBUG no relay');
rawCb(new Error("'" + state._confpath + "' is missing 'relay'")); err = new Error("'" + state._confpath + "' is missing 'relay'");
return; err.code = 'ENORELAY';
return PromiseA.reject(err);
} }
// TODO: we need some form of pre-authorization before connecting,
// otherwise we'll get disconnected pretty quickly
if (!(state.token || state.pretoken)) { if (!(state.token || state.pretoken)) {
console.log('DEBUG no token'); console.log('DEBUG no token');
rawCb(null, null); err = new Error("no jwt token or preauthorization");
return; err.code = 'ENOAUTH';
return PromiseA.reject(err);
} }
if (myRemote) { return PromiseA.resolve().then(function () {
console.log('DEBUG has remote'); console.log('DEBUG rawStartTelebitRemote');
rawCb(null, myRemote);
return;
}
if (state.wss) { function startHelper() {
startHelper(); console.log('DEBUG startHelper');
return; greenlockHelper(state);
} // Saves the token
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
// get the wss url console.log("[DEBUG] token", typeof token, token);
function retryWssLoop(err) { //state.sortingHat = state.config.sortingHat;
myRemote = null; // { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
if (!err) {
startHelper(); return new PromiseA(function (myResolve, myReject) {
return; function reject(err) {
if (myReject) {
myReject(err);
myResolve = null;
myReject = null;
} else {
console.log('DEBUG double rejection');
}
}
function resolve(val) {
if (myResolve) {
myResolve(val);
myResolve = null;
myReject = null;
} else {
console.log('DEBUG double resolution');
}
}
function onConnect() {
console.log('DEBUG on connect');
myRemote.removeListener('error', onConnectError);
myRemote.once('error', function () {
if (!state.keepAlive) {
reject(err);
return;
}
retryLoop();
});
resolve(myRemote);
return;
}
function onConnectError(err) {
myRemote = null;
console.log('DEBUG onConnectError (will safeReload)', err);
// Likely causes:
// * DNS lookup failed (no Internet)
// * Rejected (bad authn)
if ('ENOTFOUND' === err.code) {
// DNS issue, probably network is disconnected
if (!state.keepAlive) {
reject(err);
return;
}
safeReload(10 * 1000).then(resolve).catch(reject);
return;
}
reject(err);
return;
}
function retryLoop() {
console.log('DEBUG retryLoop (will safeReload)');
if (state.keepAlive) {
safeReload(10 * 1000).then(resolve).catch(reject);
}
}
myRemote = TelebitRemote.createConnection({
relay: state.relay
, wss: state.wss
, config: state.config
, otp: state.otp
, sortingHat: state.config.sortingHat
, net: state.net
, insecure: state.insecure
, token: state.token || state.pretoken // instance
, servernames: state.servernames
, ports: state.ports
, handlers: state.handlers
, greenlockConfig: state.greenlockConfig
}, onConnect);
myRemote.once('error', onConnectError);
myRemote.once('close', retryLoop);
myRemote.on('grant', state.handlers.grant);
myRemote.on('access_token', state.handlers.access_token);
});
} }
if ('ENOTFOUND' === err.code) { if (state.wss) {
// The internet is disconnected return startHelper();
// try again, and again, and again
setTimeout(function () {
startTelebitRemote(rawCb);
}, 2 * 1000);
return;
} }
rawCb(err); // get the wss url
return; function retryWssLoop(err) {
} if (!state.keepAlive) {
return PromiseA.reject(err);
}
common.api.wss(state, function onWss(err, wss) { myRemote = null;
if (err) { if (!err) {
retryWssLoop(err); return startHelper();
return; }
if ('ENOTFOUND' === err.code) {
// The internet is disconnected
// try again, and again, and again
return safeReload(2 * 1000);
}
return PromiseA.reject(err);
} }
state.wss = wss;
startHelper(); return promiseWss(state).then(function (wss) {
state.wss = wss;
return startHelper();
}).catch(function (err) {
return retryWssLoop(err);
});
}); });
} }
@ -981,6 +1071,7 @@ state.handlers = {
function sigHandler() { function sigHandler() {
console.info('Received kill signal. Attempting to exit cleanly...'); console.info('Received kill signal. Attempting to exit cleanly...');
state.keepAlive = false;
// We want to handle cleanup properly unless something is broken in our cleanup process // We want to handle cleanup properly unless something is broken in our cleanup process
// 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

View File

@ -451,12 +451,13 @@ function TelebitRemote(state) {
// Last case means the ping we sent before didn't get a response soon enough, so we // Last case means the ping we sent before didn't get a response soon enough, so we
// need to close the websocket connection. // need to close the websocket connection.
else { else {
console.log('connection timed out'); console.info('[info] closing due to connection timeout');
wstunneler.close(1000, 'connection timeout'); wstunneler.close(1000, 'connection timeout');
} }
}; };
me.destroy = function destroy() { me.destroy = function destroy() {
console.info('[info] destroy()');
try { try {
//wstunneler.close(1000, 're-connect'); //wstunneler.close(1000, 're-connect');
wstunneler._socket.destroy(); wstunneler._socket.destroy();
@ -501,7 +502,7 @@ function TelebitRemote(state) {
initialConnect = false; initialConnect = false;
}); });
wstunneler.on('close', function () { wstunneler.on('close', function () {
console.log("DEBUG closing"); console.info("[info] [closing] received close signal from relay");
clearTimeout(priv.timeoutId); clearTimeout(priv.timeoutId);
clientHandlers.closeAll(); clientHandlers.closeAll();
@ -541,6 +542,7 @@ function TelebitRemote(state) {
clearTimeout(priv.timeoutId); clearTimeout(priv.timeoutId);
priv.timeoutId = null; priv.timeoutId = null;
} }
console.info('[info] closing due to tr.end()');
wstunneler.close(1000, 're-connect'); wstunneler.close(1000, 're-connect');
wstunneler.on('close', function () { wstunneler.on('close', function () {
me.emit('end'); me.emit('end');