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 () {
'use strict';
var PromiseA;
try {
PromiseA = require('bluebird');
} catch(e) {
PromiseA = global.Promise;
}
var pkg = require('../package.json');
var url = require('url');
@ -16,7 +23,7 @@ var camelCopy = recase.camelCopy.bind(recase);
var snakeCopy = recase.snakeCopy.bind(recase);
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);
@ -68,7 +75,9 @@ if (!confpath || /^--/.test(confpath)) {
help();
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;
try {
token = fs.readFileSync(tokenpath, 'ascii').trim();
@ -79,10 +88,6 @@ try {
var controlServer;
var myRemote;
var controllers = {};
function saveConfig(cb) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb);
}
function getServername(servernames, sub) {
if (state.servernames[sub]) {
return sub;
@ -107,6 +112,11 @@ function getServername(servernames, sub) {
}
})[0];
}
function saveConfig(cb) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb);
}
var controllers = {};
controllers.http = function (req, res, opts) {
function getAppname(pathname) {
// port number
@ -364,11 +374,9 @@ function serveControlsHelper() {
//
// without proper config
//
function saveAndReport(err/*, _tun*/) {
function saveAndReport() {
console.log('[DEBUG] saveAndReport config write', confpath);
console.log(YAML.safeDump(snakeCopy(state.config)));
if (err) { throw err; }
//myRemote = _tun;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) {
res.statusCode = 500;
@ -476,39 +484,30 @@ function serveControlsHelper() {
return;
}
if (!myRemote) {
console.log('no tunnel, starting anew');
if (!state.config.disable) {
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);
// init also means enable
delete state.config.disable;
safeStartTelebitRemote(true).then(saveAndReport).catch(handleError);
}
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(); }
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
controlServer.close(function () {
// TODO closeAll other things
process.nextTick(function () {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
setTimeout(function () {
// system daemon will restart the process
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() {
delete state.config.disable;// = undefined;
state.keepAlive = true;
// TODO XXX myRemote.active
if (myRemote) {
listSuccess();
return;
}
startTelebitRemote(function (err/*, _tun*/) {
if (err) { throw err; }
//myRemote = _tun;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: { message: "Could not save config file. Perhaps you're user doesn't have permission?" }
}));
return;
}
listSuccess();
});
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) {
err.message = "Could not save config file. Perhaps you're user doesn't have permission?";
handleError(err);
return;
}
safeStartTelebitRemote(true).then(listSuccess).catch(handleError);
});
}
function disable() {
state.config.disable = true;
state.keepAlive = false;
if (myRemote) { myRemote.end(); myRemote = null; }
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
res.setHeader('Content-Type', 'application/json');
if (err) {
res.statusCode = 500;
res.end(JSON.stringify({
"error":{"message":"Could not save config file. Perhaps you're not running as root?"}
}));
err.message = "Could not save config file. Perhaps you're user doesn't have permission?";
handleError(err);
return;
}
res.end('{"success":true}');
@ -689,18 +692,17 @@ function serveControls() {
return;
}
// This will remain in a disconnect state and wait for an init
if (!(state.config.relay && (state.config.token || state.config.pretoken))) {
console.info("[info] waiting for init/authentication (missing relay and/or token)");
return;
}
console.info("[info] connecting with stored token");
function tryAgain() {
startTelebitRemote(function (err) {
if (err) { console.error('error starting (probably internet)', err); setTimeout(tryAgain, 5 * 1000); }
});
}
tryAgain();
state.keepAlive = true;
return safeStartTelebitRemote().catch(function (/*err*/) {
// ignore, it'll keep looping anyway
});
}
function parseConfig(err, text) {
@ -777,7 +779,7 @@ function approveDomains(opts, certs, cb) {
cb(new Error("servername not found in allowed list"));
}
function greenlockHelper() {
function greenlockHelper(state) {
// TODO Check undefined vs false for greenlock config
state.greenlockConf = state.config.greenlock || {};
state.greenlockConfig = {
@ -794,131 +796,219 @@ function greenlockHelper() {
state.insecure = state.config.relay_ignore_invalid_certificates;
}
function startTelebitRemote(rawCb) {
console.log('DEBUG startTelebitRemote');
function promiseTimeout(t) {
return new PromiseA(function (resolve) {
setTimeout(resolve, t);
});
}
function startHelper() {
console.log('DEBUG startHelper');
greenlockHelper();
// Saves the token
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
var promiseWss = PromiseA.promisify(function (state, fn) {
return common.api.wss(state, fn);
});
function onError(err) {
myRemote = null;
console.log('DEBUG err', err);
// Likely causes:
// * DNS lookup failed (no Internet)
// * 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);
var trPromise;
function safeStartTelebitRemote() {
state.keepAlive = false;
if (trPromise) {
return trPromise;
}
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');
rawCb(null, null);
return;
err = new Error("connecting is disabled");
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;
if (!state.relay) {
console.log('DEBUG no relay');
rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
return;
err = new Error("'" + state._confpath + "' is missing 'relay'");
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)) {
console.log('DEBUG no token');
rawCb(null, null);
return;
err = new Error("no jwt token or preauthorization");
err.code = 'ENOAUTH';
return PromiseA.reject(err);
}
if (myRemote) {
console.log('DEBUG has remote');
rawCb(null, myRemote);
return;
}
return PromiseA.resolve().then(function () {
console.log('DEBUG rawStartTelebitRemote');
if (state.wss) {
startHelper();
return;
}
function startHelper() {
console.log('DEBUG startHelper');
greenlockHelper(state);
// Saves the token
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
// get the wss url
function retryWssLoop(err) {
myRemote = null;
if (!err) {
startHelper();
return;
console.log("[DEBUG] token", typeof token, token);
//state.sortingHat = state.config.sortingHat;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
return new PromiseA(function (myResolve, myReject) {
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) {
// The internet is disconnected
// try again, and again, and again
setTimeout(function () {
startTelebitRemote(rawCb);
}, 2 * 1000);
return;
if (state.wss) {
return startHelper();
}
rawCb(err);
return;
}
// get the wss url
function retryWssLoop(err) {
if (!state.keepAlive) {
return PromiseA.reject(err);
}
common.api.wss(state, function onWss(err, wss) {
if (err) {
retryWssLoop(err);
return;
myRemote = null;
if (!err) {
return startHelper();
}
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() {
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
// 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
// need to close the websocket connection.
else {
console.log('connection timed out');
console.info('[info] closing due to connection timeout');
wstunneler.close(1000, 'connection timeout');
}
};
me.destroy = function destroy() {
console.info('[info] destroy()');
try {
//wstunneler.close(1000, 're-connect');
wstunneler._socket.destroy();
@ -501,7 +502,7 @@ function TelebitRemote(state) {
initialConnect = false;
});
wstunneler.on('close', function () {
console.log("DEBUG closing");
console.info("[info] [closing] received close signal from relay");
clearTimeout(priv.timeoutId);
clientHandlers.closeAll();
@ -541,6 +542,7 @@ function TelebitRemote(state) {
clearTimeout(priv.timeoutId);
priv.timeoutId = null;
}
console.info('[info] closing due to tr.end()');
wstunneler.close(1000, 're-connect');
wstunneler.on('close', function () {
me.emit('end');