Compare commits

..

No commits in common. "5ce4b90bcd6783e500f7ef26c537b8b885a44a07" and "32f969cb18cf79a77870f44482dc65cf299c6306" have entirely different histories.

2 changed files with 165 additions and 260 deletions

View File

@ -2,13 +2,6 @@
(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');
@ -23,7 +16,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: {}, keepAlive: { state: false } }; var state = { homedir: os.homedir(), servernames: {}, ports: {} };
var argv = process.argv.slice(2); var argv = process.argv.slice(2);
@ -75,9 +68,7 @@ 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();
@ -88,6 +79,10 @@ 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;
@ -112,11 +107,6 @@ 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
@ -374,9 +364,11 @@ function serveControlsHelper() {
// //
// without proper config // without proper config
// //
function saveAndReport() { 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; }
//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;
@ -484,30 +476,39 @@ function serveControlsHelper() {
return; return;
} }
// init also means enable if (!myRemote) {
delete state.config.disable; console.log('no tunnel, starting anew');
safeStartTelebitRemote(true).then(saveAndReport).catch(handleError); 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);
} }
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 () {
res.setHeader('Content-Type', 'application/json'); // TODO closeAll other things
res.end(JSON.stringify({ success: true })); process.nextTick(function () {
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); });
}); });
} }
@ -535,42 +536,39 @@ 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;
if (myRemote) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { listSuccess();
if (err) { return;
err.message = "Could not save config file. Perhaps you're user doesn't have permission?"; }
handleError(err); startTelebitRemote(function (err/*, _tun*/) {
return; if (err) { throw err; }
} //myRemote = _tun;
// TODO XXX myRemote.active fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (myRemote) { 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(); listSuccess();
return; });
}
safeStartTelebitRemote(true).then(listSuccess).catch(handleError);
}); });
} }
function disable() { function disable() {
state.config.disable = true; state.config.disable = true;
state.keepAlive.state = 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) {
err.message = "Could not save config file. Perhaps you're user doesn't have permission?"; res.statusCode = 500;
handleError(err); res.end(JSON.stringify({
"error":{"message":"Could not save config file. Perhaps you're not running as root?"}
}));
return; return;
} }
res.end('{"success":true}'); res.end('{"success":true}');
@ -691,16 +689,18 @@ 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");
return safeStartTelebitRemote().catch(function (/*err*/) { function tryAgain() {
// ignore, it'll keep looping anyway startTelebitRemote(function (err) {
}); if (err) { console.error('error starting (probably internet)', err); setTimeout(tryAgain, 5 * 1000); }
});
}
tryAgain();
} }
function parseConfig(err, text) { function parseConfig(err, text) {
@ -777,7 +777,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(state) { function greenlockHelper() {
// 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,221 +794,131 @@ function greenlockHelper(state) {
state.insecure = state.config.relay_ignore_invalid_certificates; state.insecure = state.config.relay_ignore_invalid_certificates;
} }
function promiseTimeout(t) { function startTelebitRemote(rawCb) {
return new PromiseA(function (resolve) { console.log('DEBUG startTelebitRemote');
setTimeout(resolve, t);
});
}
var promiseWss = PromiseA.promisify(function (state, fn) { function startHelper() {
return common.api.wss(state, fn); console.log('DEBUG startHelper');
}); greenlockHelper();
// Saves the token
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
var trPromise; function onError(err) {
function safeStartTelebitRemote(forceOn) { myRemote = null;
// whatever is currently going will not restart console.log('DEBUG err', err);
state.keepAlive.state = false; // Likely causes:
if (trPromise && !forceOn) { return trPromise; } // * DNS lookup failed (no Internet)
// * Rejected (bad authn)
// if something is running, this will kill it if ('ENOTFOUND' === err.code) {
// (TODO option to use known-good active instead of restarting) // DNS issue, probably network is disconnected
// this won't restart either setTimeout(function () {
trPromise = rawStartTelebitRemote(state.keepAlive); startTelebitRemote(rawCb);
trPromise.then(function () { }, 10 * 1000);
trPromise = null; return;
}).catch(function () { }
// this will restart if ('function' === typeof rawCb) {
state.keepAlive = { state: true }; rawCb(err);
trPromise = rawStartTelebitRemote(state.keepAlive); } else {
trPromise.then(function () { console.error('Unhandled TelebitRemote Error:');
trPromise = null; console.error(err);
}).catch(function () { }
console.log('DEBUG state.keepAlive turned off and remote quit');
trPromise = null;
});
});
return trPromise;
}
function rawStartTelebitRemote(keepAlive) {
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; console.log("[DEBUG] token", typeof token, token);
// TODO state.keepAlive? //state.sortingHat = state.config.sortingHat;
return promiseTimeout(delay).then(function () { // { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
return rawStartTelebitRemote(keepAlive); 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) { if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
console.log('DEBUG disabled or incapable'); console.log('DEBUG disabled or incapable');
err = new Error("connecting is disabled"); rawCb(null, null);
err.code = 'EDISABLED'; return;
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');
err = new Error("'" + state._confpath + "' is missing 'relay'"); rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
err.code = 'ENORELAY'; return;
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');
err = new Error("no jwt token or preauthorization"); rawCb(null, null);
err.code = 'ENOAUTH'; return;
return PromiseA.reject(err);
} }
return PromiseA.resolve().then(function () { if (myRemote) {
console.log('DEBUG rawStartTelebitRemote'); console.log('DEBUG has remote');
rawCb(null, myRemote);
return;
}
function startHelper() { if (state.wss) {
console.log('DEBUG startHelper'); startHelper();
greenlockHelper(state); return;
// Saves the token }
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
console.log("[DEBUG] token", typeof token, token); // get the wss url
//state.sortingHat = state.config.sortingHat; function retryWssLoop(err) {
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig } myRemote = null;
if (!err) {
return new PromiseA(function (myResolve, myReject) { startHelper();
function reject(err) { return;
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 (!keepAlive.state) {
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 (!keepAlive.state) {
reject(err);
return;
}
safeReload(10 * 1000).then(resolve).catch(reject);
return;
}
reject(err);
return;
}
function retryLoop() {
console.log('DEBUG retryLoop (will safeReload)');
if (keepAlive.state) {
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 (state.wss) { if ('ENOTFOUND' === err.code) {
return startHelper(); // The internet is disconnected
// try again, and again, and again
setTimeout(function () {
startTelebitRemote(rawCb);
}, 2 * 1000);
return;
} }
// get the wss url rawCb(err);
function retryWssLoop(err) { return;
if (!keepAlive.state) { }
return PromiseA.reject(err);
}
myRemote = null; common.api.wss(state, function onWss(err, wss) {
if (!err) { if (err) {
return startHelper(); retryWssLoop(err);
} 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;
return promiseWss(state).then(function (wss) { startHelper();
state.wss = wss;
return startHelper();
}).catch(function (err) {
return retryWssLoop(err);
});
}); });
} }
@ -1070,17 +980,14 @@ state.handlers = {
}; };
function sigHandler() { function sigHandler() {
process.removeListener('SIGINT', sigHandler);
console.info('Received kill signal. Attempting to exit cleanly...'); console.info('Received kill signal. Attempting to exit cleanly...');
state.keepAlive.state = 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
// the signal again and exit the way it normally would. // the signal again and exit the way it normally would.
process.removeListener('SIGINT', sigHandler);
if (myRemote) { if (myRemote) {
myRemote.end(); myRemote.end();
myRemote = null;
} }
if (controlServer) { if (controlServer) {
controlServer.close(); controlServer.close();

View File

@ -451,13 +451,12 @@ 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.info('[info] closing due to connection timeout'); console.log('connection timed out');
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();
@ -502,7 +501,7 @@ function TelebitRemote(state) {
initialConnect = false; initialConnect = false;
}); });
wstunneler.on('close', function () { wstunneler.on('close', function () {
console.info("[info] [closing] received close signal from relay"); console.log("DEBUG closing");
clearTimeout(priv.timeoutId); clearTimeout(priv.timeoutId);
clientHandlers.closeAll(); clientHandlers.closeAll();
@ -542,7 +541,6 @@ 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');