Compare commits

..

13 Commits

6 changed files with 603 additions and 506 deletions

View File

@ -6,10 +6,18 @@ var pkg = require('../package.json');
var os = require('os');
//var url = require('url');
var fs = require('fs');
var path = require('path');
var http = require('http');
//var https = require('https');
var YAML = require('js-yaml');
var TOML = require('toml');
var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8'));
/*
if ('function' !== typeof TOML.stringify) {
TOML.stringify = require('json2toml');
}
*/
var recase = require('recase').create({});
var camelCopy = recase.camelCopy.bind(recase);
//var snakeCopy = recase.snakeCopy.bind(recase);
@ -20,6 +28,9 @@ var common = require('../lib/cli-common.js');
var argv = process.argv.slice(2);
var argIndex = argv.indexOf('--config');
if (-1 === argIndex) {
argIndex = argv.indexOf('-c');
}
var confpath;
var useTty;
var state = {};
@ -35,50 +46,13 @@ if (-1 !== argIndex) {
}
function help() {
//console.info('');
//console.info('Telebit Remote v' + pkg.version);
console.info('');
console.info('Usage:');
console.info('');
console.info('\ttelebit [--config <path>] <module> <module-options>');
console.info('');
console.info('Examples:');
console.info('');
//console.info('\ttelebit init # bootstrap the config files');
//console.info('');
console.info('\ttelebit status # whether enabled or disabled');
console.info('\ttelebit enable # allow incoming connections');
console.info('\ttelebit disable # disallow incoming connections');
console.info('');
console.info('\ttelebit list # list rules for servernames and ports');
console.info('');
console.info('\ttelebit http none # remove all https handlers');
console.info('\ttelebit http 3000 # forward all https traffic to port 3000');
console.info('\ttelebit http /module/path # load a node module to handle all https traffic');
console.info('');
console.info('\ttelebit http none example.com # remove https handler from example.com');
console.info('\ttelebit http 3001 sub.example.com # forward https traffic for sub.example.com to port 3001');
console.info('\ttelebit http /module/path sub # forward https traffic for sub.example.com to port 3001');
console.info('');
console.info('\ttelebit tcp none # remove all tcp handlers');
console.info('\ttelebit tcp 5050 # forward all tcp to port 5050');
console.info('\ttelebit tcp /module/path # handle all tcp with a node module');
console.info('');
console.info('\ttelebit tcp none 6565 # remove tcp handler from external port 6565');
console.info('\ttelebit tcp 5050 6565 # forward external port 6565 to local 5050');
console.info('\ttelebit tcp /module/path 6565 # handle external port 6565 with a node module');
console.info('');
console.info('Config:');
console.info('');
console.info('\tSee https://git.coolaj86.com/coolaj86/telebit.js');
console.info('');
console.info('');
console.info(TPLS.remote.help.main.replace(/{version}/g, pkg.version));
}
var verstr = [ pkg.name + ' remote v' + pkg.version ];
if (!confpath) {
confpath = path.join(os.homedir(), '.config/telebit/telebit.yml');
verstr.push('(--config "' + confpath + '")');
verstr.push('(--config \'' + confpath.replace(new RegExp('^' + os.homedir()), '~') + '\')');
}
if (-1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) {
@ -435,6 +409,7 @@ var utils = {
console.info(body.message);
} else if ("CONFIG" === body.code) {
delete body.code;
//console.info(TOML.stringify(body));
console.info(YAML.safeDump(body));
} else {
if ('http' === body.module) {
@ -447,7 +422,7 @@ var utils = {
} else if ('tcp' === body.module) {
console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local);
} else if ('ssh' === body.module) {
console.info('> Forwarding ' + state.config.relay + ' -p ' + body.remote + ' => localhost:' + body.local);
//console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local);
console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local);
} else {
console.info(JSON.stringify(body, null, 2));
@ -604,7 +579,7 @@ function getToken(err, state) {
if (state._useTty) {
setTimeout(function () {
console.info("Some fun things to try first:\n");
console.info(" ~/telebit http 3000");
console.info(" ~/telebit http ~/public");
console.info(" ~/telebit tcp 5050");
console.info(" ~/telebit ssh auto");
console.info();
@ -670,8 +645,12 @@ function handleConfig(err, config) {
//console.log('CONFIG');
//console.log(config);
state.config = config;
var verstr = [ pkg.name + ' daemon v' + state.config.version ];
console.info(verstr.join(' '));
var verstrd = [ pkg.name + ' daemon v' + state.config.version ];
if (state.config.version && state.config.version !== pkg.version) {
console.info(verstr.join(' '), verstrd.join(' '));
} else {
console.info(verstr.join(' '));
}
if (err) { console.error(err); process.exit(101); return; }
@ -714,20 +693,20 @@ function handleConfig(err, config) {
}
function parseConfig(err, text) {
console.info("");
console.info(verstr.join(' '));
try {
state._clientConfig = JSON.parse(text || '{}');
} catch(e1) {
try {
state._clientConfig = YAML.safeLoad(text || '{}');
} catch(e2) {
console.error(e1.message);
console.error(e2.message);
process.exit(1);
return;
try {
state._clientConfig = TOML.parse(text || '');
} catch(e3) {
console.error(e1.message);
console.error(e2.message);
process.exit(1);
return;
}
}
}
@ -833,6 +812,6 @@ var parsers = {
}
};
require('fs').readFile(confpath, 'utf8', parseConfig);
fs.readFile(confpath, 'utf8', parseConfig);
}());

View File

@ -2,11 +2,35 @@
(function () {
'use strict';
//
// node telebit daemon arg1 arg2
//
if ('daemon' === process.argv[2]) {
require('./telebitd.js');
} else {
require('./telebit-remote.js');
return;
}
//
// sclient proxies
//
if ('sclient' === process.argv[2]) {
process.argv.splice(1,1);
return;
}
if ('rsync' === process.argv[2]) {
require('sclient/bin/sclient.js');
return;
}
if ('ssh' === process.argv[2] && /[\w-]+\.[a-z]{2,}/i.test(process.argv[3])) {
process.argv.splice(1,1,'sclient');
process.argv.splice(2,1,'ssh');
require('sclient/bin/sclient.js');
return;
}
//
// telebit remote
//
require('./telebit-remote.js');
}());

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');
@ -10,11 +17,16 @@ var os = require('os');
var fs = require('fs');
var common = require('../lib/cli-common.js');
var http = require('http');
var TOML = require('toml');
var YAML = require('js-yaml');
var recase = require('recase').create({});
var camelCopy = recase.camelCopy.bind(recase);
var snakeCopy = recase.snakeCopy.bind(recase);
var state = { homedir: os.homedir(), servernames: {}, ports: {} };
var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8'));
var TelebitRemote = require('../').TelebitRemote;
var state = { homedir: os.homedir(), servernames: {}, ports: {}, keepAlive: { state: false } };
var argv = process.argv.slice(2);
@ -32,20 +44,7 @@ if (-1 !== confIndex) {
var cancelUpdater = require('../lib/updater')(pkg);
function help() {
console.info('');
console.info('Telebit Daemon v' + pkg.version);
console.info('');
console.info('Usage:');
console.info('');
console.info('\ttelebitd --config <path>');
console.info('\tex: telebitd --config ~/.config/telebit/telebitd.yml');
console.info('');
console.info('');
console.info('Config:');
console.info('');
console.info('\tSee https://git.coolaj86.com/coolaj86/telebit.js');
console.info('');
console.info('');
console.info(TPLS.daemon.help.main.replace(/{version}/g, pkg.version));
}
var verstr = [ pkg.name + ' daemon v' + pkg.version ];
@ -66,7 +65,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();
@ -77,10 +78,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;
@ -105,6 +102,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
@ -362,11 +364,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;
@ -474,39 +474,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);
});
}
@ -534,39 +525,42 @@ 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;
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;
}
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;
}
// TODO XXX myRemote.active
if (myRemote) {
listSuccess();
});
return;
}
safeStartTelebitRemote(true).then(listSuccess).catch(handleError);
});
}
function disable() {
state.config.disable = true;
state.keepAlive.state = 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}');
@ -680,26 +674,22 @@ function serveControlsHelper() {
}
function serveControls() {
serveControlsHelper();
if (state.config.disable) {
console.info("[info] starting disabled");
serveControlsHelper();
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)");
serveControlsHelper();
return;
}
console.info("[info] connecting with stored token");
startTelebitRemote(function (err/*, _tun*/) {
if (err) { throw err; }
//if (_tun) { myRemote = _tun; }
setTimeout(function () {
// TODO attach handler to tunnel
serveControlsHelper();
}, 150);
return safeStartTelebitRemote().catch(function (/*err*/) {
// ignore, it'll keep looping anyway
});
}
@ -762,106 +752,252 @@ function parseConfig(err, text) {
}
}
function startTelebitRemote(rawCb) {
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
rawCb(null, null);
function approveDomains(opts, certs, cb) {
// Even though it's being tunneled by a trusted source
// we need to make sure we don't get rate-limit spammed
// with wildcard domains
// TODO: finish implementing dynamic dns for wildcard certs
if (getServername(state.servernames, opts.domains[0])) {
opts.email = state.greenlockConf.email || state.config.email;
opts.agreeTos = state.greenlockConf.agree || state.greenlockConf.agreeTos || state.config.agreeTos;
cb(null, { options: opts, certs: certs });
return;
}
cb(new Error("servername not found in allowed list"));
}
function greenlockHelper(state) {
// TODO Check undefined vs false for greenlock config
state.greenlockConf = state.config.greenlock || {};
state.greenlockConfig = {
version: state.greenlockConf.version || 'draft-11'
, server: state.greenlockConf.server || 'https://acme-v02.api.letsencrypt.org/directory'
, communityMember: state.greenlockConf.communityMember || state.config.communityMember
, telemetry: state.greenlockConf.telemetry || state.config.telemetry
, configDir: state.greenlockConf.configDir
|| (state.config.root && path.join(state.config.root, 'etc/acme'))
|| path.join(os.homedir(), '.config/telebit/acme')
// TODO, store: require(state.greenlockConf.store.name || 'le-store-certbot').create(state.greenlockConf.store.options || {})
, approveDomains: approveDomains
};
state.insecure = state.config.relay_ignore_invalid_certificates;
}
function promiseTimeout(t) {
return new PromiseA(function (resolve) {
setTimeout(resolve, t);
});
}
var promiseWss = PromiseA.promisify(function (state, fn) {
return common.api.wss(state, fn);
});
var trPromise;
function safeStartTelebitRemote(forceOn) {
// whatever is currently going will not restart
state.keepAlive.state = false;
if (trPromise && !forceOn) { return trPromise; }
// if something is running, this will kill it
// (TODO option to use known-good active instead of restarting)
// this won't restart either
trPromise = rawStartTelebitRemote(state.keepAlive);
trPromise.then(function () {
trPromise = null;
}).catch(function () {
// this will restart
state.keepAlive = { state: true };
trPromise = rawStartTelebitRemote(state.keepAlive);
trPromise.then(function () {
trPromise = null;
}).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;
// TODO state.keepAlive?
return promiseTimeout(delay).then(function () {
return rawStartTelebitRemote(keepAlive);
});
}
if (state.config.disable) {
console.log('DEBUG disabled or incapable');
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) {
rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
return;
console.log('DEBUG no relay');
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)) {
rawCb(null, null);
return;
console.log('DEBUG no token');
err = new Error("no jwt token or preauthorization");
err.code = 'ENOAUTH';
return PromiseA.reject(err);
}
if (myRemote) {
rawCb(null, myRemote);
return;
}
return PromiseA.resolve().then(function () {
console.log('DEBUG rawStartTelebitRemote');
// get the wss url
common.api.wss(state, function (err, wss) {
if (err) { rawCb(err); return; }
state.wss = wss;
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);
// 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);
//state.sortingHat = state.config.sortingHat;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
state.greenlockConf = state.config.greenlock || {};
state.sortingHat = state.config.sortingHat;
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');
}
}
// TODO sortingHat.print(); ?
// TODO Check undefined vs false for greenlock config
var TelebitRemote = require('../').TelebitRemote;
state.greenlockConfig = {
version: state.greenlockConf.version || 'draft-11'
, server: state.greenlockConf.server || 'https://acme-v02.api.letsencrypt.org/directory'
, communityMember: state.greenlockConf.communityMember || state.config.communityMember
, telemetry: state.greenlockConf.telemetry || state.config.telemetry
, configDir: state.greenlockConf.configDir
|| (state.config.root && path.join(state.config.root, 'etc/acme'))
|| path.join(os.homedir(), '.config/telebit/acme')
// TODO, store: require(state.greenlockConf.store.name || 'le-store-certbot').create(state.greenlockConf.store.options || {})
, approveDomains: function (opts, certs, cb) {
// Certs being renewed are listed in certs.altnames
if (certs) {
opts.domains = certs.altnames;
cb(null, { options: opts, certs: certs });
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;
}
// Even though it's being tunneled by a trusted source
// we need to make sure we don't get rate-limit spammed
// with wildcard domains
// TODO: finish implementing dynamic dns for wildcard certs
if (getServername(state.servernames, opts.domains[0])) {
opts.email = state.greenlockConf.email || state.config.email;
opts.agreeTos = state.greenlockConf.agree || state.greenlockConf.agreeTos || state.config.agreeTos;
cb(null, { options: opts, certs: certs });
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;
}
//cb(new Error("servername not found in allowed list"));
}
};
state.insecure = state.config.relay_ignore_invalid_certificates;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
function retryLoop() {
console.log('DEBUG retryLoop (will safeReload)');
if (keepAlive.state) {
safeReload(10 * 1000).then(resolve).catch(reject);
}
}
console.log("[DEBUG] token", typeof token, token);
myRemote = TelebitRemote.createConnection({
relay: state.relay
, wss: state.wss
, config: state.config
, otp: state.otp
, sortingHat: state.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 () {
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);
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) {
return startHelper();
}
// get the wss url
function retryWssLoop(err) {
if (!keepAlive.state) {
return PromiseA.reject(err);
}
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);
}
return promiseWss(state).then(function (wss) {
state.wss = wss;
return startHelper();
}).catch(function (err) {
return retryWssLoop(err);
});
});
}
@ -924,14 +1060,17 @@ state.handlers = {
};
function sigHandler() {
process.removeListener('SIGINT', sigHandler);
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
// 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.
process.removeListener('SIGINT', sigHandler);
if (myRemote) {
myRemote.end();
myRemote = null;
}
if (controlServer) {
controlServer.close();

87
lib/en-us.toml Normal file
View File

@ -0,0 +1,87 @@
[remote]
[remote.help]
main = "telebit remote v{version}
Telebit is a tool for helping you access your devices and share your stuff.
Usage:
telebit [flags] <command> [arguments]
The flags are:
--config <path> specify config file (default is ~/.config/telebit/telebit.yml)
The commands are:
status show status and configuration info
enable turn on remote access and sharing
disable turn off remote access and sharing
http access files, folders, and local apps via https (secure)
ssh (local) enable remote access to this device with ssh-over-https
ssh (remote) access devices via ssh-over-https (telebit, stunnel, openssl, etc)
tcp forward tcp locally
Use \"telebit help [command]\" for more information about a command.
Additional help topics:
config config file format and settings
ssh (proxy) ssh over https and proxy commands
ftp secure ftp file transfer between devices
rsync rsync over https and proxy commands
vpn home network access and private web browsing via socks5
daemon telebit daemon secure background service
relay telebit secure relay, hosted, and self-hosting options
Copyright 2015-2018 https://telebit.cloud MPL-2.0 Licensed"
http = "usage: telebit http <path/port/none> [subdomain]
'telebit http' is the fastest way to share files, folders, and local apps.
http <DIR> [subdomain] serve a file, folder, or node express app
ex: telebit http ~/Public pub securely host ~/Public as pub.johndoe.telebit.io
http <PORT> [subdomain] forward all https traffic to a local app
ex: telebit http 3000 app publicize localhost:3000 as app.johndoe.telebit.io
http none [subdomain] remove secure http access for (any or all) subdomain(s)
ex: telebit http none remove all https access
"
tcp = "
usage: telebit tcp <path/port/none>
'telebit tcp' is provided for seemless connectivity to legacy apps
tcp <local> [remote] forward tcp to <local> from <remote>
ex: telebit tcp 5050 6565 forward tcp port 6565 locally to port 5050
tcp <path> [remote] show ftp-style directory listing
ex: telebit tcp ~/Public show listing of ~/Public
tcp none [remote] disable tcp access for [remote] port
ex: telebit tcp none 6565 remove access to port 6565
See also sclient <https://telebit.cloud/sclient> for connecting to legacy apps
with telebit-upscaled secure https access.
"
[daemon]
[daemon.help]
main = "telebit daemon v{version}
Usage:
telebit daemon --config <path>
ex: telebit daemon --config ~/.config/telebit/telebitd.yml
Additional help topics:
config config file format and settings
remote telebit cli remote control
Copyright 2015-2018 https://telebit.cloud MPL-2.0 Licensed"

View File

@ -27,14 +27,16 @@ function TelebitRemote(state) {
}
EventEmitter.call(this);
var me = this;
var priv = {};
var defaultHttpTimeout = (2 * 60);
var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
//var defaultHttpTimeout = (2 * 60);
//var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
var activityTimeout = 6 * 1000;
var pongTimeout = state.pongTimeout || 10*1000;
// Allow the tunnel client to be created with no token. This will prevent the connection from
// being established initialy and allows the caller to use `.append` for the first token so
// they can get a promise that will provide feedback about invalid tokens.
var tokens = [];
priv.tokens = [];
var auth;
if(!state.sortingHat) {
state.sortingHat = "./sorting-hat.js";
@ -43,7 +45,7 @@ function TelebitRemote(state) {
if ('undefined' === state.token) {
throw new Error("passed string 'undefined' as token");
}
tokens.push(state.token);
priv.tokens.push(state.token);
}
var wstunneler;
@ -51,11 +53,11 @@ function TelebitRemote(state) {
var authsent = false;
var initialConnect = true;
var localclients = {};
priv.localclients = {};
var pausedClients = [];
var clientHandlers = {
add: function (conn, cid, tun) {
localclients[cid] = conn;
priv.localclients[cid] = conn;
console.info("[connect] new client '" + cid + "' for '" + tun.name + ":" + tun.serviceport + "' "
+ "(" + clientHandlers.count() + " clients)");
@ -80,9 +82,9 @@ function TelebitRemote(state) {
// down the data we are getting to send over. We also want to pause all active connections
// if any connections are paused to make things more fair so one connection doesn't get
// stuff waiting for all other connections to finish because it tried writing near the border.
var bufSize = wsHandlers.sendMessage(Packer.packHeader(tun, chunk));
var bufSize = sendMessage(Packer.packHeader(tun, chunk));
// Sending 2 messages instead of copying the buffer
var bufSize2 = wsHandlers.sendMessage(chunk);
var bufSize2 = sendMessage(chunk);
if (pausedClients.length || (bufSize + bufSize2) > 1024*1024) {
// console.log('[onLocalData] paused connection', cid, 'to allow websocket to catch up');
conn.pause();
@ -95,7 +97,7 @@ function TelebitRemote(state) {
console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon");
conn.tunnelClosing = true;
if (!sentEnd) {
wsHandlers.sendMessage(Packer.packHeader(tun, null, 'end'));
sendMessage(Packer.packHeader(tun, null, 'end'));
sentEnd = true;
}
});
@ -103,22 +105,22 @@ function TelebitRemote(state) {
console.info("[onLocalError] connection '" + cid + "' errored:", err);
if (!sentEnd) {
var packBody = true;
wsHandlers.sendMessage(Packer.packHeader(tun, {message: err.message, code: err.code}, 'error', packBody));
sendMessage(Packer.packHeader(tun, {message: err.message, code: err.code}, 'error', packBody));
sentEnd = true;
}
});
conn.on('close', function onLocalClose(hadErr) {
delete localclients[cid];
delete priv.localclients[cid];
console.log('[onLocalClose] closed "' + cid + '" read:'+conn.tunnelRead+', wrote:'+conn.tunnelWritten+' (' + clientHandlers.count() + ' clients)');
if (!sentEnd) {
wsHandlers.sendMessage(Packer.packHeader(tun, null, hadErr && 'error' || 'end'));
sendMessage(Packer.packHeader(tun, null, hadErr && 'error' || 'end'));
sentEnd = true;
}
});
}
, write: function (cid, opts) {
var conn = localclients[cid];
var conn = priv.localclients[cid];
if (!conn) {
return false;
}
@ -136,12 +138,12 @@ function TelebitRemote(state) {
if (!conn.remotePaused && conn.bufferSize > 1024*1024) {
var packBody = true;
wsHandlers.sendMessage(Packer.packHeader(opts, conn.tunnelRead, 'pause', packBody));
sendMessage(Packer.packHeader(opts, conn.tunnelRead, 'pause', packBody));
conn.remotePaused = true;
conn.once('drain', function () {
var packBody = true;
wsHandlers.sendMessage(Packer.packHeader(opts, conn.tunnelRead, 'resume', packBody));
sendMessage(Packer.packHeader(opts, conn.tunnelRead, 'resume', packBody));
conn.remotePaused = false;
});
}
@ -149,13 +151,13 @@ function TelebitRemote(state) {
}
, closeSingle: function (cid) {
if (!localclients[cid]) {
if (!priv.localclients[cid]) {
return;
}
console.log('[closeSingle]', cid);
PromiseA.resolve().then(function () {
var conn = localclients[cid];
var conn = priv.localclients[cid];
conn.tunnelClosing = true;
conn.end();
@ -173,41 +175,49 @@ function TelebitRemote(state) {
});
});
}).then(function () {
if (localclients[cid]) {
if (priv.localclients[cid]) {
console.warn('[closeSingle]', cid, 'connection still present after calling `end`');
localclients[cid].destroy();
priv.localclients[cid].destroy();
return timeoutPromise(500);
}
}).then(function () {
if (localclients[cid]) {
if (priv.localclients[cid]) {
console.error('[closeSingle]', cid, 'connection still present after calling `destroy`');
delete localclients[cid];
delete priv.localclients[cid];
}
}).catch(function (err) {
console.error('[closeSingle] failed to close connection', cid, err.toString());
delete localclients[cid];
delete priv.localclients[cid];
});
}
, closeAll: function () {
console.log('[closeAll]');
Object.keys(localclients).forEach(function (cid) {
Object.keys(priv.localclients).forEach(function (cid) {
clientHandlers.closeSingle(cid);
});
}
, count: function () {
return Object.keys(localclients).length;
return Object.keys(priv.localclients).length;
}
};
var pendingCommands = {};
function sendMessage(msg) {
// There is a chance that this occurred after the websocket was told to close
// and before it finished, in which case we don't need to log the error.
if (wstunneler.readyState !== wstunneler.CLOSING) {
wstunneler.send(msg, {binary: true});
return wstunneler.bufferedAmount;
}
}
function sendCommand(name) {
var id = Math.ceil(1e9 * Math.random());
var cmd = [id, name].concat(Array.prototype.slice.call(arguments, 1));
if (state.debug) { console.log('[DEBUG] command sending', cmd); }
var packBody = true;
wsHandlers.sendMessage(Packer.packHeader(null, cmd, 'control', packBody));
sendMessage(Packer.packHeader(null, cmd, 'control', packBody));
setTimeout(function () {
if (pendingCommands[id]) {
console.warn('command', name, id, 'timed out');
@ -230,24 +240,6 @@ function TelebitRemote(state) {
});
}
function sendAllTokens() {
if (auth) {
authsent = true;
sendCommand('auth', auth).catch(function (err) { console.error('1', err); });
}
tokens.forEach(function (jwtoken) {
if (state.debug) { console.log('[DEBUG] send token'); }
authsent = true;
sendCommand('add_token', jwtoken)
.catch(function (err) {
console.error('failed re-adding token', jwtoken, 'after reconnect', err);
// Not sure if we should do something like remove the token here. It worked
// once or it shouldn't have stayed in the list, so it's less certain why
// it would have failed here.
});
});
}
function noHandler(cmd) {
console.warn("[telebit] state.handlers['" + cmd[1] + "'] not set");
console.warn(cmd[2]);
@ -303,7 +295,21 @@ function TelebitRemote(state) {
if (cmd[1] === 'hello') {
if (state.debug) { console.log('[DEBUG] hello received'); }
sendAllTokens();
if (auth) {
authsent = true;
sendCommand('auth', auth).catch(function (err) { console.error('1', err); });
}
priv.tokens.forEach(function (jwtoken) {
if (state.debug) { console.log('[DEBUG] send token'); }
authsent = true;
sendCommand('add_token', jwtoken)
.catch(function (err) {
console.error('failed re-adding token', jwtoken, 'after reconnect', err);
// Not sure if we should do something like remove the token here. It worked
// once or it shouldn't have stayed in the list, so it's less certain why
// it would have failed here.
});
});
if (connCallback) {
connCallback();
}
@ -331,7 +337,7 @@ function TelebitRemote(state) {
}
var packBody = true;
wsHandlers.sendMessage(Packer.packHeader(null, [-cmd[0], err], 'control', packBody));
sendMessage(Packer.packHeader(null, [-cmd[0], err], 'control', packBody));
}
, onconnection: function (tun) {
@ -369,28 +375,28 @@ function TelebitRemote(state) {
, onpause: function (opts) {
var cid = Packer.addrToId(opts);
if (localclients[cid]) {
console.log("[TunnelPause] pausing '"+cid+"', remote received", opts.data.toString(), 'of', localclients[cid].tunnelWritten, 'sent');
localclients[cid].manualPause = true;
localclients[cid].pause();
if (priv.localclients[cid]) {
console.log("[TunnelPause] pausing '"+cid+"', remote received", opts.data.toString(), 'of', priv.localclients[cid].tunnelWritten, 'sent');
priv.localclients[cid].manualPause = true;
priv.localclients[cid].pause();
} else {
console.log('[TunnelPause] remote tried pausing finished connection', cid);
// Often we have enough latency that we've finished sending before we're told to pause, so
// don't worry about sending back errors, since we won't be sending data over anyway.
// var packBody = true;
// wsHandlers.sendMessage(Packer.packHeader(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error', packBody));
// sendMessage(Packer.packHeader(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error', packBody));
}
}
, onresume: function (opts) {
var cid = Packer.addrToId(opts);
if (localclients[cid]) {
console.log("[TunnelResume] resuming '"+cid+"', remote received", opts.data.toString(), 'of', localclients[cid].tunnelWritten, 'sent');
localclients[cid].manualPause = false;
localclients[cid].resume();
if (priv.localclients[cid]) {
console.log("[TunnelResume] resuming '"+cid+"', remote received", opts.data.toString(), 'of', priv.localclients[cid].tunnelWritten, 'sent');
priv.localclients[cid].manualPause = false;
priv.localclients[cid].resume();
} else {
console.log('[TunnelResume] remote tried resuming finished connection', cid);
// var packBody = true;
// wsHandlers.sendMessage(Packer.packHeader(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error', packBody));
// sendMessage(Packer.packHeader(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error', packBody));
}
}
@ -407,56 +413,74 @@ function TelebitRemote(state) {
, _onConnectError: function (cid, opts, err) {
console.info("[_onConnectError] opening '" + cid + "' failed because " + err.message);
wsHandlers.sendMessage(Packer.packHeader(opts, null, 'error'));
sendMessage(Packer.packHeader(opts, null, 'error'));
}
};
var lastActivity;
var timeoutId;
var wsHandlers = {
refreshTimeout: function () {
lastActivity = Date.now();
priv.timeoutId = null;
priv.lastActivity = Date.now();
priv.refreshTimeout = function refreshTimeout() {
priv.lastActivity = Date.now();
};
priv.checkTimeout = function checkTimeout() {
if (!wstunneler) {
console.warn('checkTimeout called when websocket already closed');
return;
}
, checkTimeout: function () {
if (!wstunneler) {
console.warn('checkTimeout called when websocket already closed');
return;
}
// Determine how long the connection has been "silent", ie no activity.
var silent = Date.now() - lastActivity;
// Determine how long the connection has been "silent", ie no activity.
var silent = Date.now() - priv.lastActivity;
// If we have had activity within the last activityTimeout then all we need to do is
// call this function again at the soonest time when the connection could be timed out.
if (silent < activityTimeout) {
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout-silent);
}
// Otherwise we check to see if the pong has also timed out, and if not we send a ping
// and call this function again when the pong will have timed out.
else if (silent < activityTimeout + pongTimeout) {
console.log('pinging tunnel server');
try {
wstunneler.ping();
} catch (err) {
console.warn('failed to ping tunnel server', err);
}
timeoutId = setTimeout(wsHandlers.checkTimeout, pongTimeout);
}
// 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');
wstunneler.close(1000, 'connection timeout');
}
// If we have had activity within the last activityTimeout then all we need to do is
// call this function again at the soonest time when the connection could be timed out.
if (silent < activityTimeout) {
priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout-silent);
}
, onOpen: function () {
// Otherwise we check to see if the pong has also timed out, and if not we send a ping
// and call this function again when the pong will have timed out.
else if (silent < activityTimeout + pongTimeout) {
console.log('pinging tunnel server');
try {
wstunneler.ping();
} catch (err) {
console.warn('failed to ping tunnel server', err);
}
priv.timeoutId = setTimeout(priv.checkTimeout, pongTimeout);
}
// 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.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();
} catch(e) {
// ignore
}
};
me.connect = function connect() {
if (!priv.tokens.length && state.config.email) {
auth = TelebitRemote._tokenFromState(state);
}
priv.timeoutId = null;
var machine = Packer.create(packerHandlers);
console.info("[connect] '" + (state.wss || state.relay) + "'");
var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
// XXXXXX
wstunneler.on('open', function () {
console.info("[open] connected to '" + (state.wss || state.relay) + "'");
wsHandlers.refreshTimeout();
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout);
me.emit('connect');
priv.refreshTimeout();
priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout);
wstunneler._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
@ -473,22 +497,13 @@ function TelebitRemote(state) {
conn.resume();
}
});
pausedClients.length = 0;
});
//Call either Open or Reconnect handlers.
if(state.handlers.onOpen && initialConnect) {
state.handlers.onOpen();
} else if (state.handlers.onReconnect && !initialConnect) {
state.handlers.onReconnect();
}
initialConnect = false;
}
, onClose: function () {
clearTimeout(timeoutId);
wstunneler = null;
});
wstunneler.on('close', function () {
console.info("[info] [closing] received close signal from relay");
clearTimeout(priv.timeoutId);
clientHandlers.closeAll();
var error = new Error('websocket connection closed before response');
@ -500,101 +515,20 @@ function TelebitRemote(state) {
connCallback(error);
}
if (!authenticated) {
if(state.handlers.onError) {
var err = new Error('Failed to connect on first attempt... check authentication');
state.handlers.onError(err);
}
if(state.handlers.onClose) {
state.handlers.onClose();
}
console.info('[close] failed on first attempt... check authentication.');
timeoutId = null;
}
else if (tokens.length) {
if(state.handlers.onDisconnect) {
state.handlers.onDisconnect();
}
console.info('[retry] disconnected and waiting...');
timeoutId = setTimeout(connect, 5000);
} else {
if(state.handlers.onClose) {
state.handlers.onClose();
}
}
}
, onError: function (err) {
if ('ENOTFOUND' === err.code) {
// DNS issue, probably network is disconnected
timeoutId = setTimeout(connect, 90 * 1000);
return;
}
console.error("[tunnel error] " + err.message);
console.error(err);
if (connCallback) {
connCallback(err);
}
}
, sendMessage: function (msg) {
if (wstunneler) {
try {
wstunneler.send(msg, {binary: true});
return wstunneler.bufferedAmount;
} catch (err) {
// There is a chance that this occurred after the websocket was told to close
// and before it finished, in which case we don't need to log the error.
if (wstunneler.readyState !== wstunneler.CLOSING) {
console.warn('[sendMessage] error sending websocket message', err);
}
}
}
}
};
var connPromise;
me.connect = function connect() {
if (wstunneler) {
console.warn('attempted to connect with connection already active');
return;
}
if (!tokens.length) {
if (state.config.email) {
auth = {
subject: state.config.email
, subject_scheme: 'mailto'
// TODO create domains list earlier
, scope: Object.keys(state.config.servernames || {}).join(',')
, otp: state.otp
, hostname: os.hostname()
// Used for User-Agent
, os_type: os.type()
, os_platform: os.platform()
, os_release: os.release()
, os_arch: os.arch()
};
}
}
timeoutId = null;
var machine = Packer.create(packerHandlers);
console.info("[connect] '" + (state.wss || state.relay) + "'");
var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
// XXXXXX
wstunneler.on('open', function () {
me.emit('connect');
wsHandlers.onOpen();
me.emit('close');
});
wstunneler.on('error', function (err) {
me.emit('error', err);
});
wstunneler.on('close', wsHandlers.onClose);
wstunneler.on('error', wsHandlers.onError);
// Our library will automatically handle sending the pong respose to ping requests.
wstunneler.on('ping', wsHandlers.refreshTimeout);
wstunneler.on('pong', wsHandlers.refreshTimeout);
wstunneler.on('ping', priv.refreshTimeout);
wstunneler.on('pong', function () {
console.log('DEBUG got pong');
priv.refreshTimeout();
});
wstunneler.on('message', function (data, flags) {
wsHandlers.refreshTimeout();
priv.refreshTimeout();
if (data.error || '{' === data[0]) {
console.log(data);
return;
@ -603,103 +537,35 @@ function TelebitRemote(state) {
});
};
me.end = function() {
tokens.length = 0;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
priv.tokens.length = 0;
if (priv.timeoutId) {
clearTimeout(priv.timeoutId);
priv.timeoutId = null;
}
if (wstunneler) {
try {
wstunneler.close(1000, 're-connect');
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) {
// 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);
console.info('[info] closing due to tr.end()');
wstunneler.close(1000, 're-connect');
wstunneler.on('close', function () {
me.emit('end');
});
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();
}
tokens.splice(index);
}
var prom = sendCommand('delete_token', token);
prom.catch(function (err) {
console.error('clearing token', token, 'failed:', err);
});
return prom;
};
}
TelebitRemote.prototype = EventEmitter.prototype;
TelebitRemote._tokenFromState = function (state) {
return {
subject: state.config.email
, subject_scheme: 'mailto'
// TODO create domains list earlier
, scope: Object.keys(state.config.servernames || {}).join(',')
, otp: state.otp
, hostname: os.hostname()
// Used for User-Agent
, os_type: os.type()
, os_platform: os.platform()
, os_release: os.release()
, os_arch: os.arch()
};
};
TelebitRemote.create = function (opts) {
return new TelebitRemote(opts);

View File

@ -63,12 +63,14 @@
"ps-list": "^5.0.0",
"recase": "^1.0.4",
"redirect-https": "^1.1.5",
"sclient": "^1.4.1",
"serve-index": "^1.9.1",
"serve-static": "^1.13.2",
"serve-tpl-attachment": "^1.0.4",
"sni": "^1.0.0",
"socket-pair": "^1.0.3",
"ws": "^2.3.1"
"toml": "^0.4.1",
"ws": "^6.0.0"
},
"trulyOptionalDependencies": {
"bluebird": "^3.5.1"