Compare commits

..

31 Commits

Author SHA1 Message Date
a714d7a7c5 chimney (log cleanup) 2018-09-13 23:42:04 -06:00
5d099b36a8 fix restart on close/error (TODO: double cehck enable/disable) 2018-09-12 09:55:07 -06:00
961090635c Merge branch 'v1' into next 2018-09-12 03:35:30 -06:00
7e21c85e82 merge with master 2018-09-12 03:35:22 -06:00
52805be470 Merge branch 'master' into v1 2018-09-12 03:33:57 -06:00
3726798062 update docs 2018-09-12 03:33:38 -06:00
ed34adb1a7 use sclient v1.4 2018-09-12 03:30:10 -06:00
313b8f194b WIP simplify output 2018-09-11 02:03:50 -06:00
bcc8b957d4 add toml and sclient 2018-09-11 02:02:36 -06:00
f2f85cfa18 add sclient with inverse ssh proxying 2018-09-11 02:02:15 -06:00
5ce4b90bcd WIP promisify bugfix enable 2018-09-06 03:39:21 -06:00
53ee77d8d3 WIP promisify (bug: enable acts as toggle) 2018-09-06 03:11:26 -06:00
32f969cb18 bugfix: 0-index, duh 2018-09-05 13:33:01 -06:00
02a53f681f don't destroy empty socket 2018-09-05 01:21:47 -06:00
9b0d758a8b WIP reconnect on network change 2018-09-05 01:18:12 -06:00
d8aedb39c2 WIP refactor TelebitRemote with EventEmitters and Duplexes 2018-09-04 00:31:01 -06:00
d39ebf88a2 chimney 2018-09-03 23:18:59 -06:00
918eeb49d7 merge with telebit-remote 2018-09-03 23:13:45 -06:00
a361a76258 merge with master 2018-09-03 23:04:41 -06:00
4210243c35 one bin to rule them all 2018-09-03 23:02:11 -06:00
290d192bc9 bin/telebit.js -> bin/telebit-remote.js 2018-09-03 23:01:43 -06:00
6cd2d0ac16 begin refactor TelebitRemote 2018-09-03 22:56:52 -06:00
3fa6d15848 latest serve-tpl-attachment with security update 2018-08-12 04:10:01 -06:00
73c4444b51 update serve-tpl-attachment dep 2018-08-12 04:02:58 -06:00
687b2a3567 v0.20.0-wip: enable direct download of files via serve-index/serve-static 2018-08-12 03:45:13 -06:00
fb8aa998b3 don't expect data on 'connection' event 2018-08-08 03:14:40 -06:00
4a1f020100 Merge branch 'master' into v1 2018-08-08 02:31:46 -06:00
e72a5f1f56 WIP mproxy v2.x, still missing connection event 2018-08-08 02:30:38 -06:00
3c068debc0 display advanced config for relay when present 2018-08-08 01:39:27 -06:00
bd8d32d8ec WIP v0.20.x: add onconnection handler 2018-08-08 01:11:29 -06:00
78407f2a3e WIP v0.20.x: switch to proxy-packer v2.x and comment readable 2018-08-08 00:51:16 -06:00
8 changed files with 804 additions and 576 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 ];
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,22 +693,22 @@ 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) {
try {
state._clientConfig = TOML.parse(text || '');
} catch(e3) {
console.error(e1.message);
console.error(e2.message);
process.exit(1);
return;
}
}
}
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
common._init(
@ -790,6 +769,9 @@ var parsers = {
answers[parts[0]] = parts[1];
});
if (answers.relay) {
console.info("using --relay " + answers.relay);
}
// things that aren't straight-forward copy-over
if (!answers.advanced && !answers.relay) {
answers.relay = 'telebit.cloud';
@ -830,6 +812,6 @@ var parsers = {
}
};
require('fs').readFile(confpath, 'utf8', parseConfig);
fs.readFile(confpath, 'utf8', parseConfig);
}());

28
bin/telebit.js Executable file → Normal file
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,21 +65,19 @@ 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();
console.log('[DEBUG] access_token', typeof token, token);
//console.log('[DEBUG] access_token', typeof token, token);
} catch(e) {
// ignore
}
var controlServer;
var tun;
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
@ -352,22 +354,19 @@ function serveControlsHelper() {
res.end(JSON.stringify(dumpy));
}
if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) {
function getConfigOnly() {
var resp = JSON.parse(JSON.stringify(state.config));
resp.version = pkg.version;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(resp));
return;
}
//
// 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; }
tun = _tun;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
if (err) {
res.statusCode = 500;
@ -380,7 +379,8 @@ function serveControlsHelper() {
listSuccess();
});
}
if (/\b(init|config)\b/.test(opts.pathname)) {
function initOrConfig() {
var conf = {};
if (!opts.body) {
res.statusCode = 422;
@ -458,7 +458,7 @@ function serveControlsHelper() {
}
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
console.log('aborting for some reason');
console.warn('missing config');
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
@ -474,66 +474,42 @@ function serveControlsHelper() {
return;
}
if (tun) {
console.log('ending existing tunnel, starting anew');
tun.end(function () {
console.log('success ending');
rawTunnel(saveAndReport);
});
tun = null;
setTimeout(function () {
if (!tun) {
console.log('failed to end, but starting anyway');
rawTunnel(saveAndReport);
}
}, 3000);
} else {
console.log('no tunnel, starting anew');
rawTunnel(saveAndReport);
}
return;
// init also means enable
delete state.config.disable;
safeStartTelebitRemote(true).then(saveAndReport).catch(handleError);
}
if (/restart/.test(opts.pathname)) {
tun.end();
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(); }
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);
});
});
return;
}
//
// Check for proper config
//
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
function invalidConfig() {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: { code: "E_CONFIG", message: "Invalid config file. Please run 'telebit init'" }
}));
return;
}
//
// With proper config
//
if (/http/.test(opts.pathname)) {
controllers.http(req, res, opts);
return;
}
if (/tcp/.test(opts.pathname)) {
controllers.tcp(req, res, opts);
return;
}
if (/save|commit/.test(opts.pathname)) {
function saveAndCommit() {
state.config.servernames = state.servernames;
state.config.ports = state.ports;
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
@ -547,69 +523,114 @@ function serveControlsHelper() {
}
listSuccess();
});
return;
}
if (/ssh/.test(opts.pathname)) {
controllers.ssh(req, res, opts);
return;
function handleError(err) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: { message: err.message, code: err.code }
}));
}
if (/enable/.test(opts.pathname)) {
function enable() {
delete state.config.disable;// = undefined;
if (tun) {
listSuccess();
return;
}
rawTunnel(function (err, _tun) {
if (err) { throw err; }
tun = _tun;
state.keepAlive.state = true;
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?" }
}));
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);
});
}
if (/disable/.test(opts.pathname)) {
function disable() {
state.config.disable = true;
if (tun) { tun.end(); tun = null; }
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}');
});
return;
}
if (/status/.test(opts.pathname)) {
function getStatus() {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(
{ status: (state.config.disable ? 'disabled' : 'enabled')
, ready: ((state.config.relay && (state.config.token || state.config.agreeTos)) ? true : false)
, active: !!tun
, active: !!myRemote
, connected: 'maybe (todo)'
, version: pkg.version
, servernames: state.servernames
}
));
return;
}
if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) {
getConfigOnly();
return;
}
if (/\b(init|config)\b/.test(opts.pathname)) {
initOrConfig();
return;
}
if (/restart/.test(opts.pathname)) {
restart();
return;
}
//
// Check for proper config
//
if (!state.config.relay || !state.config.email || !state.config.agreeTos) {
invalidConfig();
return;
}
//
// With proper config
//
if (/http/.test(opts.pathname)) {
controllers.http(req, res, opts);
return;
}
if (/tcp/.test(opts.pathname)) {
controllers.tcp(req, res, opts);
return;
}
if (/save|commit/.test(opts.pathname)) {
saveAndCommit();
return;
}
if (/ssh/.test(opts.pathname)) {
controllers.ssh(req, res, opts);
return;
}
if (/enable/.test(opts.pathname)) {
enable();
return;
}
if (/disable/.test(opts.pathname)) {
disable();
return;
}
if (/status/.test(opts.pathname)) {
getStatus();
return;
}
if (/list/.test(opts.pathname)) {
listSuccess();
return;
@ -618,6 +639,7 @@ function serveControlsHelper() {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}}));
});
if (fs.existsSync(state._ipc.path)) {
fs.unlinkSync(state._ipc.path);
}
@ -653,27 +675,23 @@ function serveControlsHelper() {
}
function serveControls() {
serveControlsHelper();
if (state.config.disable) {
console.info("[info] starting disabled");
return;
}
if (state.config.relay && (state.config.token || state.config.pretoken)) {
console.info("[info] connecting with stored token");
rawTunnel(function (err, _tun) {
if (err) { throw err; }
if (_tun) { tun = _tun; }
setTimeout(function () {
// TODO attach handler to tunnel
serveControlsHelper();
}, 150);
});
return;
} else {
// 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;
}
serveControlsHelper();
console.info("[info] connecting with stored token");
return safeStartTelebitRemote().catch(function (/*err*/) {
// ignore, it'll keep looping anyway
});
}
function parseConfig(err, text) {
@ -735,61 +753,7 @@ function parseConfig(err, text) {
}
}
function rawTunnel(rawCb) {
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
rawCb(null, null);
return;
}
state.relay = state.config.relay;
if (!state.relay) {
rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
return;
}
if (!(state.token || state.pretoken)) {
rawCb(null, null);
return;
}
if (tun) {
rawCb(null, tun);
return;
}
common.api.wss(state, function (err, wss) {
if (err) { rawCb(err); return; }
state.wss = wss;
// Saves the token
// state.handlers.access_token({ jwt: token });
// Adds the token to the connection
// tun.append(token);
state.greenlockConf = state.config.greenlock || {};
state.sortingHat = state.config.sortingHat;
// TODO sortingHat.print(); ?
// TODO Check undefined vs false for greenlock config
var remote = require('../');
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 });
return;
}
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
@ -801,19 +765,200 @@ function rawTunnel(rawCb) {
return;
}
//cb(new Error("servername not found in allowed list"));
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 () {
//console.log("I'm RIGHT HERE!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
state.keepAlive.state = true;
trPromise = null;
}).catch(function () {
//console.log("I FAILED US ALL!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
// 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) {
//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)) {
//console.log('DEBUG no token');
err = new Error("no jwt token or preauthorization");
err.code = 'ENOAUTH';
return PromiseA.reject(err);
}
return PromiseA.resolve().then(function () {
//console.log('DEBUG rawStartTelebitRemote');
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);
//console.log("[DEBUG] token", typeof token, token);
//state.sortingHat = state.config.sortingHat;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
console.log("[DEBUG] token", typeof token, token);
tun = remote.connect({
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) {
//console.log('[DEBUG] pre-resolve');
if (myResolve) {
myResolve(val);
myResolve = null;
myReject = null;
} else {
//console.log('DEBUG double resolution');
}
}
function onConnect() {
console.info('[connect] relay established');
myRemote.removeListener('error', onConnectError);
myRemote.once('error', function () {
if (!keepAlive.state) {
reject(err);
return;
}
retryLoop();
});
resolve(myRemote);
return;
}
function onConnectError(err) {
myRemote = null;
// 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;
}
console.warn('[Warn] onConnectError: network error, will retry', err);
safeReload(10 * 1000).then(resolve).catch(reject);
return;
}
console.error('[Error] onConnectError: no retry (possibly bad auth)', err);
reject(err);
return;
}
function retryLoop() {
console.warn('[Warn] disconnected. Will retry?', keepAlive.state);
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.sortingHat
, sortingHat: state.config.sortingHat
, net: state.net
, insecure: state.insecure
, token: state.token || state.pretoken // instance
@ -821,9 +966,45 @@ function rawTunnel(rawCb) {
, ports: state.ports
, handlers: state.handlers
, greenlockConfig: state.greenlockConfig
});
}, onConnect);
rawCb(null, tun);
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);
});
});
}
@ -885,14 +1066,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 (tun) {
tun.end();
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

@ -11,6 +11,7 @@ var WebSocket = require('ws');
var sni = require('sni');
var Packer = require('proxy-packer');
var os = require('os');
var EventEmitter = require('events').EventEmitter;
function timeoutPromise(duration) {
return new PromiseA(function (resolve) {
@ -18,15 +19,24 @@ function timeoutPromise(duration) {
});
}
function _connect(state) {
function TelebitRemote(state) {
// jshint latedef:false
var defaultHttpTimeout = (2 * 60);
var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
if (!(this instanceof TelebitRemote)) {
return new TelebitRemote(state);
}
EventEmitter.call(this);
var me = this;
var priv = {};
//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";
@ -35,7 +45,7 @@ function _connect(state) {
if ('undefined' === state.token) {
throw new Error("passed string 'undefined' as token");
}
tokens.push(state.token);
priv.tokens.push(state.token);
}
var wstunneler;
@ -43,19 +53,24 @@ function _connect(state) {
var authsent = false;
var initialConnect = true;
var localclients = {};
priv.localclients = {};
var pausedClients = [];
var clientHandlers = {
add: function (conn, cid, tun) {
localclients[cid] = conn;
console.info("[connect] new client '" + cid + "' for '" + tun.name + ":" + tun.serviceport + "' "
priv.localclients[cid] = conn;
console.info("[connect] new client '" + tun.name + ":" + tun.serviceport + "' for '" + cid + "'"
+ "(" + clientHandlers.count() + " clients)");
conn.tunnelCid = cid;
if (tun.data) {
conn.tunnelRead = tun.data.byteLength;
} else {
conn.tunnelRead = 0;
}
conn.tunnelWritten = 0;
conn.on('data', function onLocalData(chunk) {
//var chunk = conn.read();
if (conn.tunnelClosing) {
console.warn("[onLocalData] received data for '"+cid+"' over socket after connection was ended");
return;
@ -67,8 +82,10 @@ function _connect(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.pack(tun, chunk));
if (pausedClients.length || bufSize > 1024*1024) {
var bufSize = sendMessage(Packer.packHeader(tun, chunk));
// Sending 2 messages instead of copying the buffer
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();
pausedClients.push(conn);
@ -77,32 +94,33 @@ function _connect(state) {
var sentEnd = false;
conn.on('end', function onLocalEnd() {
console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon");
//console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon");
conn.tunnelClosing = true;
if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(tun, null, 'end'));
sendMessage(Packer.packHeader(tun, null, 'end'));
sentEnd = true;
}
});
conn.on('error', function onLocalError(err) {
console.info("[onLocalError] connection '" + cid + "' errored:", err);
if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(tun, {message: err.message, code: err.code}, 'error'));
var packBody = true;
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.pack(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;
}
@ -119,11 +137,13 @@ function _connect(state) {
conn.tunnelRead += opts.data.byteLength;
if (!conn.remotePaused && conn.bufferSize > 1024*1024) {
wsHandlers.sendMessage(Packer.pack(opts, conn.tunnelRead, 'pause'));
var packBody = true;
sendMessage(Packer.packHeader(opts, conn.tunnelRead, 'pause', packBody));
conn.remotePaused = true;
conn.once('drain', function () {
wsHandlers.sendMessage(Packer.pack(opts, conn.tunnelRead, 'resume'));
var packBody = true;
sendMessage(Packer.packHeader(opts, conn.tunnelRead, 'resume', packBody));
conn.remotePaused = false;
});
}
@ -131,13 +151,13 @@ function _connect(state) {
}
, closeSingle: function (cid) {
if (!localclients[cid]) {
if (!priv.localclients[cid]) {
return;
}
console.log('[closeSingle]', cid);
//console.log('[closeSingle]', cid);
PromiseA.resolve().then(function () {
var conn = localclients[cid];
var conn = priv.localclients[cid];
conn.tunnelClosing = true;
conn.end();
@ -155,40 +175,49 @@ function _connect(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); }
wsHandlers.sendMessage(Packer.pack(null, cmd, 'control'));
var packBody = true;
sendMessage(Packer.packHeader(null, cmd, 'control', packBody));
setTimeout(function () {
if (pendingCommands[id]) {
console.warn('command', name, id, 'timed out');
@ -211,24 +240,6 @@ function _connect(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]);
@ -236,6 +247,23 @@ function _connect(state) {
var connCallback;
function hyperPeek(tun) {
var m;
var str;
if (tun.data) {
if ('http' === tun.service) {
str = tun.data.toString();
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
tun._name = tun._hostname = (m && m[1].toLowerCase() || '').split(':')[0];
}
else if ('https' === tun.service || 'tls' === tun.service) {
tun._name = tun._servername = sni(tun.data);
} else {
tun._name = '';
}
}
}
var packerHandlers = {
oncontrol: function (opts) {
var cmd, err;
@ -267,7 +295,21 @@ function _connect(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();
}
@ -294,28 +336,19 @@ function _connect(state) {
err = { message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' };
}
wsHandlers.sendMessage(Packer.pack(null, [-cmd[0], err], 'control'));
var packBody = true;
sendMessage(Packer.packHeader(null, [-cmd[0], err], 'control', packBody));
}
, onmessage: function (tun) {
, onconnection: function (tun) {
var cid = tun._id = Packer.addrToId(tun);
var str;
var m;
if ('http' === tun.service) {
str = tun.data.toString();
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
tun._name = tun._hostname = (m && m[1].toLowerCase() || '').split(':')[0];
}
else if ('https' === tun.service || 'tls' === tun.service) {
tun._name = tun._servername = sni(tun.data);
} else {
tun._name = '';
}
// this data should have been gathered already as part of the proxy protocol
// but if it's available again here we can double check
hyperPeek(tun);
if (clientHandlers.write(cid, tun)) { return; }
wstunneler.pause();
// TODO use readable streams instead
wstunneler._socket.pause();
require(state.sortingHat).assign(state, tun, function (err, conn) {
if (err) {
err.message = err.message.replace(/:tun_id/, tun._id);
@ -324,32 +357,46 @@ function _connect(state) {
}
clientHandlers.add(conn, cid, tun);
if (tun.data) { conn.write(tun.data); }
wstunneler.resume();
wstunneler._socket.resume();
});
}
, onmessage: function (tun) {
var cid = tun._id = Packer.addrToId(tun);
var handled;
hyperPeek(tun);
handled = clientHandlers.write(cid, tun);
// quasi backwards compat
if (!handled) { console.log("[debug] did not get 'connection' event"); packerHandlers.onconnection(tun); }
}
, 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.
// wsHandlers.sendMessage(Packer.pack(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'));
// var packBody = true;
// 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);
// wsHandlers.sendMessage(Packer.pack(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error'));
// var packBody = true;
// sendMessage(Packer.packHeader(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error', packBody));
}
}
@ -366,56 +413,74 @@ function _connect(state) {
, _onConnectError: function (cid, opts, err) {
console.info("[_onConnectError] opening '" + cid + "' failed because " + err.message);
wsHandlers.sendMessage(Packer.pack(opts, null, 'error'));
sendMessage(Packer.packHeader(opts, null, 'error'));
}
};
var lastActivity;
var timeoutId;
var wsHandlers = {
refreshTimeout: function () {
lastActivity = Date.now();
}
, checkTimeout: function () {
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;
}
// Determine how long the connection has been "silent", ie no activity.
var silent = Date.now() - lastActivity;
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);
priv.timeoutId = setTimeout(priv.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');
//console.log('DEBUG: pinging tunnel server');
try {
wstunneler.ping();
} catch (err) {
console.warn('failed to ping tunnel server', err);
}
timeoutId = setTimeout(wsHandlers.checkTimeout, pongTimeout);
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.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();
} 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);
, onOpen: function () {
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
@ -432,22 +497,13 @@ function _connect(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');
@ -459,67 +515,44 @@ function _connect(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();
}
}
}
me.emit('close');
});
wstunneler.on('error', function (err) {
me.emit('error', err);
});
, onError: function (err) {
if ('ENOTFOUND' === err.code) {
// DNS issue, probably network is disconnected
timeoutId = setTimeout(connect, 90 * 1000);
// Our library will automatically handle sending the pong respose to ping requests.
wstunneler.on('ping', priv.refreshTimeout);
wstunneler.on('pong', function () {
//console.log('DEBUG received pong');
priv.refreshTimeout();
});
wstunneler.on('message', function (data, flags) {
priv.refreshTimeout();
if (data.error || '{' === data[0]) {
console.log(data);
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);
}
}
}
}
machine.fns.addChunk(data, flags);
});
};
function connect() {
if (wstunneler) {
console.warn('attempted to connect with connection already active');
return;
me.end = function() {
priv.tokens.length = 0;
if (priv.timeoutId) {
clearTimeout(priv.timeoutId);
priv.timeoutId = null;
}
if (!tokens.length) {
if (state.config.email) {
auth = {
console.info('[info] closing due to tr.end()');
wstunneler.close(1000, 're-connect');
wstunneler.on('close', function () {
me.emit('end');
});
};
}
TelebitRemote.prototype = EventEmitter.prototype;
TelebitRemote._tokenFromState = function (state) {
return {
subject: state.config.email
, subject_scheme: 'mailto'
// TODO create domains list earlier
@ -532,131 +565,19 @@ function _connect(state) {
, 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 });
wstunneler.on('open', wsHandlers.onOpen);
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('message', function (data, flags) {
wsHandlers.refreshTimeout();
if (data.error || '{' === data[0]) {
console.log(data);
return;
}
machine.fns.addChunk(data, flags);
});
}
connect();
var connPromise;
return {
end: function(cb) {
tokens.length = 0;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (wstunneler) {
try {
wstunneler.close(cb);
} catch(e) {
console.error("[error] wstunneler.close()");
console.error(e);
}
}
}
, 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);
});
return prom;
}
, 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.create = function (opts) {
return new TelebitRemote(opts);
};
}
TelebitRemote.createConnection = function (opts, cb) {
var tunnel = TelebitRemote.create(opts);
tunnel.connect(opts);
tunnel.once('connect', cb);
return tunnel;
};
TelebitRemote.connect = TelebitRemote.createConnection;
module.exports.connect = _connect;
module.exports.createConnection = _connect;
module.exports.TelebitRemote = TelebitRemote;
}());

View File

@ -59,7 +59,7 @@ module.exports.print = function (config) {
};
module.exports.assign = function (state, tun, cb) {
console.log('first message from', tun);
//console.log('first message from', tun);
var net = state.net || require('net');
function trySsh(tun, cb) {
@ -328,15 +328,15 @@ module.exports.assign = function (state, tun, cb) {
try {
handler = require(handlerpath);
console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
console.info("Trying to handle '" + handle + ":" + id + "' with '" + handlerpath + "'");
} catch(e1) {
try {
handler = require(path.join(localshare, handlerpath));
console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
console.info("Skip. (couldn't require('" + handlerpath + "'):", e1.message + ")");
console.info("Trying to handle '" + handle + ":" + id + "' with '" + handlerpath + "'");
} catch(e2) {
console.error("Failed to require('" + handlerpath + "'):", e1.message);
console.error("Failed to require('" + path.join(localshare, handlerpath) + "'):", e2.message);
console.warn("Trying static and index handlers for '" + handle + ":" + id + "'");
console.info("Skip. (couldn't require('" + path.join(localshare, handlerpath) + "'):", e2.message + ")");
console.info("Last chance! (using static and index handlers for '" + handle + ":" + id + "')");
handler = null;
// fallthru
}
@ -362,21 +362,47 @@ module.exports.assign = function (state, tun, cb) {
state._serveIndex = require('serve-index');
var serveIndex;
var serveStatic;
var dlStatic;
if (isFile) {
serveStatic = state._serveStatic(path.dirname(conf.handler), { dotfiles: 'allow', index: [ 'index.html' ] });
dlStatic = state._serveStatic(path.dirname(conf.handler), { acceptRanges: false, dotfiles: 'allow', index: [ 'index.html' ] });
serveIndex = function (req, res, next) { next(); };
isFile = path.basename(conf.handler);
} else {
serveStatic = state._serveStatic(conf.handler, { dotfiles: 'allow', index: [ 'index.html' ] });
serveIndex = state._serveIndex(conf.handler, { hidden: true, icons: true, view: 'tiles' });
dlStatic = state._serveStatic(conf.handler, { acceptRanges: false, dotfiles: 'allow', index: [ 'index.html' ] });
serveIndex = state._serveIndex(conf.handler, {
hidden: true, icons: true
, template: require('serve-tpl-attachment')({ privatefiles: 'ignore' })
});
}
handler = function (req, res) {
var qIndex = req.url.indexOf('?');
var fIndex;
var fname;
if (-1 === qIndex) {
qIndex = req.url.length;
}
req.querystring = req.url.substr(qIndex);
req.url = req.url.substr(0, qIndex);
req.query = require('querystring').parse(req.querystring.substr(1));
if (isFile) {
req.url = '/' + isFile;
}
//console.log('[req.query]', req.url, req.query);
if (req.query.download) {
fIndex = req.url.lastIndexOf('/');
fname = req.url.substr(fIndex + 1);
res.setHeader('Content-Disposition', 'attachment; filename="'+decodeURIComponent(fname)+'"');
res.setHeader('Content-Type', 'application/octet-stream');
dlStatic(req, res, function () {
serveIndex(req, res, state._finalHandler(req, res));
});
} else {
serveStatic(req, res, function () {
serveIndex(req, res, state._finalHandler(req, res));
});
}
};
handlerservers[conf.handler] = http.createServer(handler);
handlerservers[conf.handler].emit('connection', tlsSocket);
@ -426,7 +452,7 @@ module.exports.assign = function (state, tun, cb) {
return;
}
console.log('https invokeHandler');
//console.log('https invokeHandler');
invokeHandler(conf, tlsSocket, tun, id);
});
});

View File

@ -8,6 +8,7 @@ module.exports = function (pkg) {
https.get(url, function (resp) {
var str = '';
resp.on('data', function (chunk) {
//var chunk = conn.read();
str += chunk.toString('utf8');
});
resp.on('end', function () {

View File

@ -1,6 +1,6 @@
{
"name": "telebit",
"version": "0.19.28",
"version": "0.20.0-wip",
"description": "Break out of localhost. Connect to any device from anywhere over any tcp port or securely in a browser. A secure tunnel. A poor man's reverse VPN.",
"main": "lib/remote.js",
"files": [
@ -59,15 +59,18 @@
"js-yaml": "^3.11.0",
"jsonwebtoken": "^7.1.9",
"mkdirp": "^0.5.1",
"proxy-packer": "^1.4.3",
"proxy-packer": "^2.0.2",
"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.2.3"
"toml": "^0.4.1",
"ws": "^6.0.0"
},
"trulyOptionalDependencies": {
"bluebird": "^3.5.1"