Compare commits

...

63 Commits
stable ... wip

Author SHA1 Message Date
AJ ONeal 1726e137b8 [WIP] getting closer 5 years ago
AJ ONeal ffc95b4ddf [WIP] more new account 5 years ago
AJ ONeal 7f49650c48 [WIP] new account 5 years ago
AJ ONeal a5c448902e cleanup 5 years ago
AJ ONeal ae452367c0 whitespace 5 years ago
AJ ONeal 7a9cc7cb77 WIP: account creation api 5 years ago
AJ ONeal 16be216c99 fix whitespace 5 years ago
AJ ONeal 53cc3ccaba add basic key handling to server 5 years ago
AJ ONeal 58dab177da shims for jwt and jws authentication 5 years ago
AJ ONeal 1fda5b15d0 bugfix key storage 5 years ago
AJ ONeal 1c9396216a generate and store private key for telebit-remote 5 years ago
AJ ONeal 21115b9543 address npm's security audit warnings 5 years ago
AJ ONeal 693a524549 use eggspress for routing 5 years ago
AJ ONeal 583ffabdff minor updates 5 years ago
AJ ONeal 75f538fa16 minor cleanup, throw on previously unchecked error 6 years ago
AJ ONeal d5a622444e don't require subdomain 6 years ago
AJ ONeal 0996d78ecd add note on ssh 6 years ago
AJ ONeal 7758e3b5ed forms now work on enter key 6 years ago
AJ ONeal fff8f318c0 handle shares a little better 6 years ago
AJ ONeal 535c9732c6 rearrange a few things 6 years ago
AJ ONeal 34bcf79d98 use default sshd_config on windows also 6 years ago
AJ ONeal 142fb0942c show local ssh config 6 years ago
AJ ONeal 7f18482566 more exact checking 6 years ago
AJ ONeal 40921b58ff function to test ssh safety 6 years ago
AJ ONeal d743c51d86 change to status after auth 6 years ago
AJ ONeal b18a6aa01c show error when status is error 6 years ago
AJ ONeal 08c18b8c94 async transitions, but don't delay 6 years ago
AJ ONeal 2679a20d1c updates for config file 6 years ago
AJ ONeal ce20b058e6 updates to status page 6 years ago
AJ ONeal c5e7811028 refactoring for web ui and resumable state 6 years ago
AJ ONeal 4b64490bdc handle config object (or old array) and some web ui fixes 6 years ago
AJ ONeal 075342920d fixes... 6 years ago
AJ ONeal 61a5af2124 chimney 6 years ago
AJ ONeal e6b7ba575f wip trying to get auth request and token... 6 years ago
AJ ONeal 9e1c9c00ca web ui updates 6 years ago
AJ ONeal b81f0ecede use details and summary for advanced 6 years ago
AJ ONeal 40d78b463f use local relay to avoid cors 6 years ago
AJ ONeal f0222baff6 wip adapt common init for web setup 6 years ago
AJ ONeal f2e60dae5e wip i18n cli 6 years ago
AJ ONeal 07e9bd7ed9 wip web setup 6 years ago
AJ ONeal 9827267620 Merge branch 'master' into next 6 years ago
AJ ONeal 81ee4b27a5 move email prompt to locale doc file 6 years ago
AJ ONeal b75552a287 add uptime info 6 years ago
AJ ONeal 66758f4dbf post merge bugfix 6 years ago
AJ ONeal 311e82b9b6 complete merge 6 years ago
AJ ONeal 79d01e2c31 show port on status 6 years ago
AJ ONeal 0bdaacb8aa allow port in remote client 6 years ago
AJ ONeal 26939a62cf add route function 6 years ago
AJ ONeal a6e4bda317 move control handler to own function 6 years ago
AJ ONeal e0ea17a377 merge master 6 years ago
AJ ONeal be7a895dc7 WIP web remote client 6 years ago
AJ ONeal 8f7e865d49 move from json-in-querystring to POST bodies 6 years ago
AJ ONeal 20ed109aeb merge ssh defaults bugfix from master 6 years ago
AJ ONeal ebd4f530f2 v0.21.0-wip.1: refactoring rc 6 years ago
AJ ONeal 68dac05d47 Merge branch 'master' into next 6 years ago
AJ ONeal 6c9d13f155 update require path 6 years ago
AJ ONeal aad592c893 clarification 6 years ago
AJ ONeal 82f5545d72 wip minor refactoring 6 years ago
AJ ONeal 3e0c977511 fix a few WIP parser bugs 6 years ago
AJ ONeal c3e9bbaa5a WIP keep parsing and console output in remote, move other stuff out 6 years ago
AJ ONeal f3e5945afc Merge branch 'master' into next 6 years ago
AJ ONeal 1fc04f05b5 Merge branch 'master' into next 6 years ago
AJ ONeal e16e5a34e6 WIP launcher updates 6 years ago
  1. 6
      README.md
  2. 781
      bin/telebit-remote.js
  3. 1
      bin/telebit.js
  4. 1153
      bin/telebitd.js
  5. 60
      lib/admin/documentation/index.html
  6. 234
      lib/admin/index.html
  7. 513
      lib/admin/js/app.js
  8. 116
      lib/admin/js/telebit-token.js
  9. 338
      lib/admin/js/telebit.js
  10. 10947
      lib/admin/js/vue.js
  11. 4
      lib/cli-common.js
  12. 10
      lib/daemon/index.js
  13. 102
      lib/eggspress.js
  14. 8
      lib/en-us.toml
  15. 19
      lib/jwt-test.js
  16. 43
      lib/jwt.js
  17. 37
      lib/keystore-fallback.js
  18. 22
      lib/keystore-test.js
  19. 49
      lib/keystore.js
  20. 175
      lib/rc/index.js
  21. 76
      lib/ssh.js
  22. 1124
      package-lock.json
  23. 12
      package.json
  24. 34
      usr/share/install-launcher.js
  25. 63
      usr/share/which.js

6
README.md

@ -1,6 +1,8 @@
# Telebit™ Remote
Because friends don't let friends localhost™
The T-Rex Long-Arm of the Internet
<small>because friends don't let friends localhost</small>
| Sponsored by [ppl](https://ppl.family)
| **Telebit Remote**
@ -524,7 +526,7 @@ rm -rf ~/.config/telebit ~/.local/share/telebit
Browser Library
=======
This is implemented with websockets, so you should be able to
This is implemented with websockets, so browser compatibility is a hopeful future outcome. Would love help.
LICENSE
=======

781
bin/telebit-remote.js

@ -8,11 +8,13 @@ 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'));
var JWT = require('../lib/jwt.js');
var keypairs = require('keypairs');
/*
if ('function' !== typeof TOML.stringify) {
TOML.stringify = require('json2toml');
@ -23,8 +25,12 @@ var camelCopy = recase.camelCopy.bind(recase);
//var snakeCopy = recase.snakeCopy.bind(recase);
var urequest = require('@coolaj86/urequest');
var urequestAsync = require('util').promisify(urequest);
var common = require('../lib/cli-common.js');
var defaultConfPath = path.join(os.homedir(), '.config/telebit');
var defaultConfFile = path.join(defaultConfPath, 'telebit.yml');
var argv = process.argv.slice(2);
var argIndex = argv.indexOf('--config');
@ -39,6 +45,30 @@ if (-1 === argIndex) {
}
if (-1 !== argIndex) {
confpath = argv.splice(argIndex, 2)[1];
state.configArg = confpath;
// shortname
if (state.configArg) {
if (/^[\w:\.\-]+$/.test(state.configArg)) {
state.configDir = defaultConfPath;
state.configFile = path.join(defaultConfPath, confpath + '.yml');
} else if (/[\/\\]$/.test(state.configArg)) {
state.configDir = state.configArg;
state.configFile = path.join(state.configDir, 'telebit.yml');
} else if (/[\/\\][^\.\/\\]\.[^\.\/\\]$/.test(state.configArg)) {
state.configDir = path.pathname(state.configArg);
state.configFile = state.configArg;
} else {
console.error();
console.error("Not a valid config path, file, or shortname: '%s'", state.configArg);
console.error();
console.error("Valid config options look like this:");
console.error(" Full path: ~/.config/telebit/telebit.yml (full path)");
console.error(" Directory: ~/.config/telebit/ (directory)");
console.error(" Shortname: lucky-duck (shortname)");
process.exit(37);
}
confpath = state.configFile;
}
}
argIndex = argv.indexOf('--tty');
if (-1 !== argIndex) {
@ -57,7 +87,9 @@ function help() {
var verstr = [ pkg.name + ' remote v' + pkg.version ];
if (!confpath) {
confpath = path.join(os.homedir(), '.config/telebit/telebit.yml');
state.configDir = defaultConfPath;
state.configFile = defaultConfFile;
confpath = state.configFile;
verstr.push('(--config \'' + confpath.replace(new RegExp('^' + os.homedir()), '~') + '\')');
}
@ -123,13 +155,10 @@ function askForConfig(state, mainCb) {
return;
}
if (200 !== resp.statusCode || (Buffer.isBuffer(body) || 'object' !== typeof body) || !body.api_host) {
console.warn("===================");
console.warn(" WARNING ");
console.warn("===================");
console.warn("");
console.warn("[" + resp.statusCode + "] '" + urlstr + "'");
console.warn("This server does not describe a current telebit version (but it may still work).");
console.warn("");
console.warn(TPLS.remote.setup.fail_relay_check
.replace(/{{\s*status_code\s*}}/, resp.statusCode)
.replace(/{{\s*url\s*}}/, urlstr)
);
console.warn(body);
} else if (body && body.pair_request) {
state._can_pair = true;
@ -158,7 +187,7 @@ function askForConfig(state, mainCb) {
}
];
var standardSet = [
// There are questions that we need to aks in the CLI
// There are questions that we need to ask in the CLI
// if we can't guarantee that they are being asked in the web interface
function askAgree(cb) {
if (state.config.agreeTos) { cb(); return; }
@ -235,10 +264,9 @@ function askForConfig(state, mainCb) {
//console.info("\tPrivate key (hex)");
console.info("");
rl.question('auth: ', function (resp) {
var jwt = require('jsonwebtoken');
resp = (resp || '').trim();
try {
jwt.decode(resp);
JWT.decode(resp);
state.config.token = resp;
} catch(e) {
// is not jwt
@ -305,395 +333,145 @@ function askForConfig(state, mainCb) {
next();
}
var utils = {
request: function request(opts, fn) {
if (!opts) { opts = {}; }
var service = opts.service || 'config';
var req = http.request({
socketPath: state._ipc.path
, method: opts.method || 'GET'
, path: '/rpc/' + service
}, function (resp) {
var body = '';
function finish() {
if (200 !== resp.statusCode) {
console.warn(resp.statusCode);
console.warn(body || ('get' + service + ' failed'));
//cb(new Error("not okay"), body);
return;
}
var RC;
if (!body) { fn(null, null); return; }
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
fn(null, body);
}
function parseConfig(err, text) {
function handleConfig(err, config) {
state.config = config;
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 (resp.headers['content-length']) {
resp.on('data', function (chunk) {
body += chunk.toString();
});
resp.on('end', function () {
finish();
});
} else {
finish();
}
});
req.on('error', function (err) {
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
// ECONNREFUSED - leftover socket just needs to be restarted
if (err) {
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
if (opts._taketwo) {
console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to.");
console.error(err);
return;
}
require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
if (err) { fn(err); return; }
opts._taketwo = true;
setTimeout(function () {
utils.request(opts, fn);
}, 2500);
});
return;
}
if ('ENOTSOCK' === err.code) {
console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to.");
console.error(err);
} else if ('ENOTSOCK' === err.code) {
console.error(err);
return;
} else {
console.error(err);
}
console.error(err);
process.exit(101);
return;
});
req.end();
}
, putConfig: function putConfig(service, args, fn) {
var req = http.request({
socketPath: state._ipc.path
, method: 'POST'
, path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args))
}, function (resp) {
function finish() {
if ('function' === typeof fn) {
fn(null, resp);
return;
}
}
console.info("");
if (200 !== resp.statusCode) {
console.warn("'" + service + "' may have failed."
+ " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log");
console.warn(resp.statusCode, body);
//cb(new Error("not okay"), body);
return;
//
// check for init first, before anything else
// because it has arguments that may help in
// the next steps
//
if (-1 !== argv.indexOf('init')) {
parsers.init(argv, function (err) {
if (err) {
console.error("Error while initializing config [init]:");
throw err;
}
getToken(function (err) {
if (err) {
console.error("Error while getting token [init]:");
throw err;
}
parseCli(state);
});
});
return;
}
if (!body) {
console.info("👌");
return;
}
if (!state.config.relay || !state.config.token) {
if (!state.config.relay) {
state.config.relay = 'telebit.cloud';
}
try {
body = JSON.parse(body);
} catch(e) {
// ignore
//console.log("question the user?", Date.now());
askForConfig(state, function (err, state) {
// no errors actually get passed, so this is just future-proofing
if (err) { throw err; }
if (!state.config.token && state._can_pair) {
state.config._otp = common.otp();
}
if ("AWAIT_AUTH" === body.code) {
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) {
// TODO we'll support slingshot-ing in the future
if (String(body.local) === String(parseInt(body.local, 10))) {
console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local);
} else {
console.info('> Serving ' + body.local + ' as https://' + body.remote);
}
} 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 ' + JSON.stringify(body) + ' => localhost:' + body.local);
console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local);
} else {
console.info(JSON.stringify(body, null, 2));
//console.log("done questioning:", Date.now());
if (!state.token && !state.config.token) {
if (err) {
console.error("Error while initializing config [init]:");
throw err;
}
console.info();
getToken(function (err) {
if (err) {
console.error("Error while getting token [init]:");
throw err;
}
parseCli(state);
});
} else {
parseCli(state);
}
}
var body = '';
if (resp.headers['content-length']) {
resp.on('data', function (chunk) {
body += chunk.toString();
});
resp.on('end', function () {
finish();
});
} else {
finish();
}
});
req.on('error', function (err) {
console.error('Put Config Error:');
console.error(err);
return;
});
req.end();
}
};
// Two styles:
// http 3000
// http modulename
function makeRpc(key) {
if (key !== argv[0]) {
return false;
}
utils.putConfig(argv[0], argv.slice(1));
return true;
}
function packConfig(config) {
return Object.keys(config).map(function (key) {
var val = config[key];
if ('undefined' === val) {
throw new Error("'undefined' used as a string value");
}
if ('undefined' === typeof val) {
//console.warn('[DEBUG]', key, 'is present but undefined');
});
return;
}
if (val && 'object' === typeof val && !Array.isArray(val)) {
val = JSON.stringify(val);
}
return key + ':' + val; // converts arrays to strings with ,
});
}
function getToken(err, state) {
if (err) {
console.error("Error while initializing config [init]:");
throw err;
//console.log("no questioning:");
parseCli(state);
}
state.relay = state.config.relay;
// { _otp, config: {} }
common.api.token(state, {
error: function (err/*, next*/) {
console.error("[Error] common.api.token:");
console.error(err);
function parseCli(/*state*/) {
var special = [
'false', 'none', 'off', 'disable'
, 'true', 'auto', 'on', 'enable'
];
if (-1 !== argv.indexOf('init')) {
RC.request({ service: 'list', method: 'POST', data: [] }, handleRemoteRequest('list'));
return;
}
, directory: function (dir, next) {
//console.log('[directory] Telebit Relay Discovered:');
//console.log(dir);
state._apiDirectory = dir;
next();
}
, tunnelUrl: function (tunnelUrl, next) {
//console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
state.wss = tunnelUrl;
next();
}
, requested: function (authReq, next) {
//console.log("[requested] Pairing Requested");
state.config._otp = state.config._otp = authReq.otp;
if (!state.config.token && state._can_pair) {
console.info("");
console.info("==============================================");
console.info(" Hey, Listen! ");
console.info("==============================================");
console.info(" ");
console.info(" GO CHECK YOUR EMAIL! ");
console.info(" ");
console.info(" DEVICE PAIR CODE: 0000 ".replace(/0000/g, state.config._otp));
console.info(" ");
console.info("==============================================");
console.info("");
if ([ 'ssh', 'http', 'tcp' ].some(function (key) {
if (key !== argv[0]) {
return false;
}
next();
}
, connect: function (pretoken, next) {
//console.log("[connect] Enabling Pairing Locally...");
state.config.pretoken = pretoken;
state._connecting = true;
// TODO use php-style object querification
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) {
if (err) {
state._error = err;
console.error("Error while initializing config [connect]:");
console.error(err);
return;
if (argv[1]) {
if (String(argv[1]) === String(parseInt(argv[1], 10))) {
// looks like a port
argv[1] = parseInt(argv[1], 10);
} else if (/\/|\\/.test(argv[1])) {
// looks like a path
argv[1] = path.resolve(argv[1]);
// TODO make a default assignment here
} else if (-1 === special.indexOf(argv[1])) {
console.error("Not sure what you meant by '" + argv[1] + "'.");
console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'");
return true;
}
console.info("waiting...");
next();
});
}
, offer: function (token, next) {
//console.log("[offer] Pairing Enabled by Relay");
state.config.token = token;
if (state._error) {
return;
}
state._connecting = true;
try {
require('jsonwebtoken').decode(token);
//console.log(require('jsonwebtoken').decode(token));
} catch(e) {
console.warn("[warning] could not decode token");
RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0]));
return true;
}
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) {
if (err) {
state._error = err;
console.error("Error while initializing config [offer]:");
console.error(err);
return;
}
//console.log("Pairing Enabled Locally");
next();
});
}
, granted: function (_, next) {
//console.log("[grant] Pairing complete!");
next();
}
, end: function () {
utils.putConfig('enable', [], function (err) {
if (err) { console.error(err); return; }
console.info("Success");
// workaround for https://github.com/nodejs/node/issues/21319
if (state._useTty) {
setTimeout(function () {
console.info("Some fun things to try first:\n");
console.info(" ~/telebit http ~/public");
console.info(" ~/telebit tcp 5050");
console.info(" ~/telebit ssh auto");
console.info();
console.info("Press any key to continue...");
console.info();
process.exit(0);
}, 0.5 * 1000);
return;
}
// end workaround
parseCli(state);
});
help();
return true;
})) {
return;
}
});
}
function parseCli(/*state*/) {
var special = [
'false', 'none', 'off', 'disable'
, 'true', 'auto', 'on', 'enable'
];
if (-1 !== argv.indexOf('init')) {
utils.putConfig('list', []/*, function (err) {
}*/);
return;
}
if ([ 'ssh', 'http', 'tcp' ].some(function (key) {
if (key !== argv[0]) {
return false;
}
if (argv[1]) {
if (String(argv[1]) === String(parseInt(argv[1], 10))) {
// looks like a port
argv[1] = parseInt(argv[1], 10);
} else if (/\/|\\/.test(argv[1])) {
// looks like a path
argv[1] = path.resolve(argv[1]);
// TODO make a default assignment here
} else if (-1 === special.indexOf(argv[1])) {
console.error("Not sure what you meant by '" + argv[1] + "'.");
console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'");
return true;
// Two styles:
// http 3000
// http modulename
function makeRpc(key) {
if (key !== argv[0]) {
return false;
}
utils.putConfig(argv[0], argv.slice(1));
RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0]));
return true;
}
return true;
})) {
return;
}
if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) {
return;
}
help();
process.exit(11);
}
function handleConfig(err, config) {
//console.log('CONFIG');
//console.log(config);
state.config = config;
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; }
//
// check for init first, before anything else
// because it has arguments that may help in
// the next steps
//
if (-1 !== argv.indexOf('init')) {
parsers.init(argv, getToken);
return;
}
if (!state.config.relay || !state.config.token) {
if (!state.config.relay) {
state.config.relay = 'telebit.cloud';
if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) {
return;
}
//console.log("question the user?", Date.now());
askForConfig(state, function (err, state) {
// no errors actually get passed, so this is just future-proofing
if (err) { throw err; }
if (!state.config.token && state._can_pair) {
state.config._otp = common.otp();
}
//console.log("done questioning:", Date.now());
if (!state.token && !state.config.token) {
getToken(err, state);
} else {
parseCli(state);
}
});
return;
help();
process.exit(11);
}
//console.log("no questioning:");
parseCli(state);
}
function parseConfig(err, text) {
try {
state._clientConfig = JSON.parse(text || '{}');
} catch(e1) {
@ -712,13 +490,7 @@ function parseConfig(err, text) {
}
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
common._init(
// make a default working dir and log dir
state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit')
, (state._clientConfig.root && path.join(state._clientConfig.root, 'etc'))
|| path.resolve(common.DEFAULT_CONFIG_PATH, '..')
);
state._ipc = common.pipename(state._clientConfig, true);
RC = require('../lib/rc/index.js').create(state);
if (!Object.keys(state._clientConfig).length) {
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
@ -733,7 +505,229 @@ function parseConfig(err, text) {
}
}
utils.request({ service: 'config' }, handleConfig);
function handleRemoteRequest(service, fn) {
return function (err, body) {
if ('function' === typeof fn) {
fn(err, body); // XXX was resp
return;
}
console.info("");
if (err) {
console.warn("'" + service + "' may have failed."
+ " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log");
console.warn(err.statusCode, err.message);
//cb(new Error("not okay"), body);
return;
}
if (!body) {
console.info("👌");
return;
}
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
if ("AWAIT_AUTH" === body.code) {
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) {
// TODO we'll support slingshot-ing in the future
if (body.local) {
if (String(body.local) === String(parseInt(body.local, 10))) {
console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local);
} else {
console.info('> Serving ' + body.local + ' as https://' + body.remote);
}
} else {
console.info('> Rejecting End-to-End Encrypted HTTPS for now');
}
} else if ('tcp' === body.module) {
if (body.local) {
console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local);
} else {
console.info('> Rejecting Legacy TCP');
}
} else if ('ssh' === body.module) {
//console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local);
if (body.local) {
console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local);
} else {
console.info('> Rejecting SSH-over-HTTPS for now');
}
} else if ('status' === body.module) {
// TODO funny one this one
if (body.port) {
console.info('http://localhost:' + (body.port));
}
console.info(JSON.stringify(body, null, 2));
} else {
console.info(JSON.stringify(body, null, 2));
}
console.info();
}
};
}
function getToken(fn) {
state.relay = state.config.relay;
// { _otp, config: {} }
common.api.token(state, {
error: function (err/*, next*/) {
console.error("[Error] common.api.token:");
console.error(err);
return;
}
, directory: function (dir, next) {
//console.log('[directory] Telebit Relay Discovered:');
//console.log(dir);
state._apiDirectory = dir;
next();
}
, tunnelUrl: function (tunnelUrl, next) {
//console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
state.wss = tunnelUrl;
next();
}
, requested: function (authReq, next) {
//console.log("[requested] Pairing Requested");
state.config._otp = state.config._otp = authReq.otp;
if (!state.config.token && state._can_pair) {
console.info(TPLS.remote.code.replace(/0000/g, state.config._otp));
}
next();
}
, connect: function (pretoken, next) {
//console.log("[connect] Enabling Pairing Locally...");
state.config.pretoken = pretoken;
state._connecting = true;
// TODO use php-style object querification
RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) {
if (err) {
state._error = err;
console.error("Error while initializing config [connect]:");
console.error(err);
return;
}
console.info("waiting...");
next();
}));
}
, offer: function (token, next) {
//console.log("[offer] Pairing Enabled by Relay");
state.config.token = token;
if (state._error) {
return;
}
state._connecting = true;
try {
JWT.decode(token);
//console.log(JWT.decode(token));
} catch(e) {
console.warn("[warning] could not decode token");
}
RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) {
if (err) {
state._error = err;
console.error("Error while initializing config [offer]:");
console.error(err);
return;
}
//console.log("Pairing Enabled Locally");
next();
}));
}
, granted: function (_, next) {
//console.log("[grant] Pairing complete!");
next();
}
, end: function () {
RC.request({ service: 'enable', method: 'POST', data: [] }, handleRemoteRequest('enable', function (err) {
if (err) { console.error(err); return; }
console.info("Success");
// workaround for https://github.com/nodejs/node/issues/21319
if (state._useTty) {
setTimeout(function () {
console.info("Some fun things to try first:\n");
console.info(" ~/telebit http ~/public");
console.info(" ~/telebit tcp 5050");
console.info(" ~/telebit ssh auto");
console.info();
console.info("Press any key to continue...");
console.info();
process.exit(0);
}, 0.5 * 1000);
return;
}
// end workaround
//parseCli(state);
fn();
}));
}
});
}
var bootState = {};
function bootstrap() {
// Create / retrieve account (sign-in, more or less)
// TODO hit directory resource /.well-known/openid-configuration -> acme_uri (?)
// Occassionally rotate the key just for the sake of testing the key rotation
return urequestAsync({ method: 'HEAD', url: RC.resolve('/acme/new-nonce') }).then(function (resp) {
var nonce = resp.headers['replay-nonce'];
var newAccountUrl = RC.resolve('/acme/new-acct');
return keypairs.signJws({
jwk: state.key
, protected: {
// alg will be filled out automatically
jwk: state.pub
, nonce: nonce
, url: newAccountUrl
}
, payload: JSON.stringify({
// We can auto-agree here because the client is the user agent of the primary user
termsOfServiceAgreed: true
, contact: [] // I don't think we have email yet...
//, externalAccountBinding: null
})
}).then(function (jws) {
return urequestAsync({
url: newAccountUrl
, method: 'POST'
, json: jws // TODO default to post when body is present
, headers: { "Content-Type": 'application/jose+json' }
}).then(function (resp) {
console.log(newAccountUrl, 'resp.body:');
console.log(resp.body);
if (!resp.body || 'valid' !== resp.body.status) {
throw new Error("did not successfully create or restore account");
}
return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) {
console.error(err.stack);
process.exit(27);
}).then(handleConfig);
});
});
}).catch(RC.createErrorHandler(bootstrap, bootState, function (err) {
console.error(err);
process.exit(17);
}));
}
bootstrap();
}
var parsers = {
@ -813,6 +807,41 @@ var parsers = {
}
};
fs.readFile(confpath, 'utf8', parseConfig);
var keystore = require('../lib/keystore.js').create(state);
state.keystore = keystore;
state.keystoreSecure = !keystore.insecure;
keystore.all().then(function (list) {
var keyext = '.key.jwk.json';
var key;
// TODO create map by account and index into that map to get the master key
// and sort keys in the process
list.some(function (el) {
if (keyext === el.account.slice(-keyext.length)
&& el.password.kty && el.password.kid) {
key = el.password;
return true;
}
});
if (key) {
state.key = key;
state.pub = keypairs.neuter({ jwk: key });
fs.readFile(confpath, 'utf8', parseConfig);
return;
}
return keypairs.generate().then(function (pair) {
var jwk = pair.private;
return keypairs.thumbprint({ jwk: jwk }).then(function (kid) {
jwk.kid = kid;
return keystore.set(kid + keyext, jwk).then(function () {
var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8);
console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid);
state.key = jwk;
fs.readFile(confpath, 'utf8', parseConfig);
});
});
});
});
}());

1
bin/telebit.js

@ -21,6 +21,7 @@ if ('rsync' === process.argv[2]) {
require('sclient/bin/sclient.js');
return;
}
// handle ssh client rather than ssh https tunnel
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');

1153
bin/telebitd.js

File diff suppressed because it is too large

60
lib/admin/documentation/index.html

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<title>Telebit Documentation</title>
</head>
<body>
<div class="v-app">
<h1>Telebit (Remote) Documentation</h1>
<section>
<h2>GET /api/config</h2>
<pre><code>{{ config }}</code></pre>
</section>
<section>
<h2>GET /api/status</h2>
<pre><code>{{ status }}</code></pre>
</section>
<section>
<h2>POST /api/init</h2>
<form v-on:submit.stop.prevent="initialize">
<label for="-email">Email:</label>
<input id="-email" v-model="init.email" type="text" placeholder="john@example.com">
<br>
<label for="-teletos"><input id="-teletos" v-model="init.teletos" type="checkbox">
Accept Telebit Terms of Service</label>
<br>
<label for="-letos"><input id="-letos" v-model="init.letos" type="checkbox">
Accept Let's Encrypt Terms of Service</label>
<br>
</form>
<pre><code>{{ init }}</code></pre>
</section>
<section>
<h2>POST /api/http</h2>
<pre><code>{{ http }}</code></pre>
</section>
<section>
<h2>POST /api/tcp</h2>
<pre><code>{{ tcp }}</code></pre>
</section>
<section>
<h2>POST /api/ssh</h2>
<pre><code>{{ ssh }}</code></pre>
</section>
</div>
<script src="/js/vue.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

234
lib/admin/index.html

@ -0,0 +1,234 @@
<!DOCTYPE html>
<html>
<head>
<title>Telebit Setup</title>
</head>
<body>
<script>document.body.hidden = true;</script>
<div class="v-app">
<h1>Telebit (Remote) Setup v{{ config.version }}</h1>
<section v-if="views.flash.error">
{{ views.flash.error }}
</section>
<section v-if="views.section.loading">
Loading...
</section>
<section v-if="views.section.setup">
<h2>Create Account</h2>
<form v-on:submit.stop.prevent="initialize">
<label for="-email">Email:</label>
<input id="-email" v-model="init.email" type="text" placeholder="john@example.com" required>
<br>
<label for="-teletos"><input id="-teletos" v-model="init.teletos" type="checkbox" required>
Accept Telebit Terms of Service</label>
<br>
<label for="-letos"><input id="-letos" v-model="init.letos" type="checkbox" required>
Accept Let's Encrypt Terms of Service</label>
<br>
<label for="-notifications">Notification Preferences</label>
<select id="-notifications" v-model="init.notifications">
<option value="newsletter">Occassional Newsletter</option>
<option value="important" default><strong>Important Messages Only</strong></option>
<option value="required">Required Only</option>
</select>
<small>
<p v-if="'newsletter' == init.notifications">
You'll receive a friendly note now and then in addition to the important updates.
</p>
<p v-if="'important' == init.notifications">
You'll only receive updates that we believe will be of the most value to you, and the required updates.
</p>
<p v-if="'required' == init.notifications">
You'll only receive security updates, transactional and account-related messages, and legal notices.
</p>
</small>
<details><summary><small>Advanced</small></summary>
<label for="-relay">Relay:</label>
<input id="-relay" v-model="init.relay" type="text" placeholder="telebit.cloud">
<br>
<button type="button" v-on:click="defaultRelay">Use Default</button>
<button type="button" v-on:click="betaRelay">Use Beta</button>
<br>
<br>
<label for="-acme-server">ACME (Let's Encrypt) Server:</label>
<input id="-acme-server" v-model="init.acmeServer" type="text" placeholder="https://acme-v02.api.letsencrypt.org/directory">
<br>
<button type="button" v-on:click="productionAcme">Use Production</button>
<button type="button" v-on:click="stagingAcme">Use Staging</button>
<br>
<br>
</details>
<button type="submit">Accept &amp; Continue</button>
</form>
<pre><code>{{ init }}</code></pre>
</section>
<section v-if="views.section.advanced">
<h2>Advanced Setup for {{ init.relay }}</h2>
<form v-on:submit.stop.prevent="advance">
<strong><label for="-secret">Relay Shared Secret:</label></strong>
<input id="-secret" v-model="init.secret" type="text" placeholder="ex: xxxxxxxxxxxx">
<br>
<strong><label for="-domains">Domains:</label></strong>
<br>
<small>(comma separated list of domains to use for http, tls, https, etc)</small>
<br>
<input id="-domains" v-model="init.domains" type="text" placeholder="ex: whatever.com, example.com">
<br>
<strong><label for="-ports">TCP Ports:</label></strong>
<br>
<small>(comman separated list of ports, excluding 80 and 443, typically port over 1024)</small>
<br>
<input id="-ports" v-model="init.ports" type="text" placeholder="ex: 5050, 3000, 8080">
<br>
<label for="-telemetry"><input id="-telemetry" v-model="init.telemetry" type="checkbox">
Contribute to Telebit by sharing telemetry</label>
<br>
<button type="submit">Finish</button>
</form>
<pre><code>{{ init }}</code></pre>
</section>
<section v-if="views.section.otp">
<pre><code><h2>{{ init.otp }}</h2></code></pre>
</section>
<section v-if="views.section.status">
http://localhost:{{ status.port }}
<br>
<br>
<section v-if="views.section.status_chooser">
<button v-on:click.prevent.stop="changeState('status/share')">Share Files &amp; Folders</button>
<button v-on:click.prevent.stop="changeState('status/host')">Host a Website or Webapp</button>
<button v-on:click.prevent.stop="changeState('status/access')">Remote Access via SSH</button>
</section>
<section v-if="views.section.status_access">
SSH:
<span v-if="status.ssh">{{ status.ssh }}
<button v-on:click="ssh(-1)">Disable SSH</button></span>
<span v-if="!status.ssh"><input type="text" v-model="state.ssh" placeholder="22">
<button v-on:click="ssh(state.ssh)">Enable SSH</button></span>
<br>
<br>
<div v-if="state.ssh_active">SSH is currently running</div>
<div v-if="!state.ssh_active">SSH is not currently running</div>
<br>
<div v-if="state.ssh_insecure">Password Authentication is NOT disabled.
Please consider updating your <code>sshd_config</code> and restarting ssh.
<pre><code>{{ status }}</code></pre>
</div>
<div v-if="!state.ssh_insecure">Key-Only Authentication is enabled :)</div>
<br>
<div class="alert alert-info">
<strong>Important:</strong> Accessing this device with other SSH clients:
<br>
In order to use your other ssh clients with telebit you will need to put them into
<strong>ssh+https mode</strong>.
We recommend downloading <code><a href="https://telebit.cloud/sclient/" target="_blank">sclient</a></code>
to do so, because it makes it as simple as adding <code>-o ProxyCommand="sclient %h"</code> to your
ssh command to enable ssh+https:
<pre><code>ssh -o ProxyCommand="sclient %h" {{ newHttp.name }}</code></pre>
<br>
However, most clients can also use <code>openssl s_client</code>, which does the same thing, but is
more difficult to remember:
<pre><code>proxy_cmd='openssl s_client -connect %h:443 -servername %h -quiet'
ssh -o ProxyCommand="$proxy_cmd" hot-skunk-45.telebit.io</code></pre>
</div>
</section>
<section v-if="views.section.status_share">
Path Hosting:
<ul>
<li v-for="domain in status.pathHosting">
<form v-on:submit.prevent.stop="changePathHost(domain, domain.path)">
{{ domain.name }}
<input type="text" v-model="domain.path" v-bind:placeholder="domain.handler">
<button type="submit"
v-if="domain.handler == domain.path">Save</button>
<button type="button" v-on:click="deletePathHost(domain)">X</button>
</form>
</li>
</ul>
<form v-on:submit.prevent.stop="createShare(newHttp.sub, newHttp.name, newHttp.handler)">
<input v-model="newHttp.sub" type="text" placeholder="subdomain (ex: pub)">
<select v-model="newHttp.name">
<option v-for="w in status.wildDomains" v-bind:value="w.name">{{ w.name }}</option>
</select>
<input v-model="newHttp.handler" type="text" placeholder="path (ex: ~/Public)" required>
<button>Add</button>
</form>
<br>
</section>
<section v-if="views.section.status_host">
Port Forwarding:
<ul>
<li v-for="domain in status.portForwards">
<form v-on:submit.prevent.stop="changePortForward(domain, domain._port)">
{{ domain.name }}
<input type="text" v-model="domain._port" v-bind:placeholder="domain.handler">
<button type="submit"
v-if="domain.handler == domain._port">Save</button>
<button type="button" v-on:click="deletePortForward(domain)">X</button>
</form>
</li>
</ul>
<form v-on:submit="createHost(newHttp.sub, newHttp.name, newHttp.handler)">
<input v-model="newHttp.sub" type="text" placeholder="subdomain (ex: api)">
<select v-model="newHttp.name">
<option v-for="w in status.wildDomains" v-bind:value="w.name">{{ w.name }}</option>
</select>
<input v-model="newHttp.handler" type="number" placeholder="port (ex: 3000)" required>
<button>Add</button>
</form>
</section>
<br>
Uptime: {{ statusUptime }}
<br>
Runtime: {{ statusRuntime }}
<br>
Reconnects: {{ status.reconnects }}
<details><summary><small>Advanced</small></summary>
<button v-if="!status.enabled" v-on:click="enable">Enable Traffic</button>
<button v-if="status.enabled" v-on:click="disable">Disable Traffic</button>
<br>
<br>
<pre><code>{{ status }}</code></pre>
</details>
</section>
</div>
<script src="/js/vue.js"></script>
<script src="/js/telebit.js"></script>
<script src="/js/telebit-token.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

513
lib/admin/js/app.js

@ -0,0 +1,513 @@
;(function () {
'use strict';
var Vue = window.Vue;
var Telebit = window.TELEBIT;
var api = {};
/*
function safeFetch(url, opts) {
var controller = new AbortController();
var tok = setTimeout(function () {
controller.abort();
}, 4000);
if (!opts) {
opts = {};
}
opts.signal = controller.signal;
return window.fetch(url, opts).finally(function () {
clearTimeout(tok);
});
}
*/
api.config = function apiConfig() {
return Telebit.reqLocalAsync({
url: "/api/config"
, method: "GET"
}).then(function (resp) {
var json = resp.body;
appData.config = json;
return json;
});
};
api.status = function apiStatus() {
return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) {
var json = resp.body;
return json;
});
};
api.http = function apiHttp(o) {
var opts = {
url: "/api/http"
, method: "POST"
, headers: { 'Content-Type': 'application/json' }
, json: { name: o.name, handler: o.handler, indexes: o.indexes }
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
appData.initResult = json;
return json;
}).catch(function (err) {
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
});
};
api.ssh = function apiSsh(port) {
var opts = {
url: "/api/ssh"
, method: "POST"
, headers: { 'Content-Type': 'application/json' }
, json: { port: port }
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
appData.initResult = json;
return json;
}).catch(function (err) {
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
});
};
api.enable = function apiEnable() {
var opts = {
url: "/api/enable"
, method: "POST"
//, headers: { 'Content-Type': 'application/json' }
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
console.log('enable', json);
return json;
}).catch(function (err) {
window.alert("Error: [enable] " + (err.message || JSON.stringify(err, null, 2)));
});
};
api.disable = function apiDisable() {
var opts = {
url: "/api/disable"
, method: "POST"
//, headers: { 'Content-Type': 'application/json' }
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
console.log('disable', json);
return json;
}).catch(function (err) {
window.alert("Error: [disable] " + (err.message || JSON.stringify(err, null, 2)));
});
};
function showOtp(otp, pollUrl) {
localStorage.setItem('poll_url', pollUrl);
telebitState.pollUrl = pollUrl;
appData.init.otp = otp;
changeState('otp');
}
function doConfigure() {
if (telebitState.dir.pair_request) {
telebitState._can_pair = true;
}
//
// Read config from form
//
// Create Empty Config, If Necessary
if (!telebitState.config) { telebitState.config = {}; }
if (!telebitState.config.greenlock) { telebitState.config.greenlock = {}; }
// Populate Config
if (appData.init.teletos && appData.init.letos) { telebitState.config.agreeTos = true; }
if (appData.init.relay) { telebitState.config.relay = appData.init.relay; }
if (appData.init.email) { telebitState.config.email = appData.init.email; }
if ('undefined' !== typeof appData.init.letos) { telebitState.config.greenlock.agree = appData.init.letos; }
if ('newsletter' === appData.init.notifications) {
telebitState.config.newsletter = true; telebitState.config.communityMember = true;
}
if ('important' === appData.init.notifications) { telebitState.config.communityMember = true; }
if (appData.init.acmeVersion) { telebitState.config.greenlock.version = appData.init.acmeVersion; }
if (appData.init.acmeServer) { telebitState.config.greenlock.server = appData.init.acmeServer; }
// Temporary State
telebitState._otp = Telebit.otp();
appData.init.otp = telebitState._otp;
return Telebit.authorize(telebitState, showOtp).then(function () {
return changeState('status');
});
}
// TODO test for internet connectivity (and telebit connectivity)
var DEFAULT_RELAY = 'telebit.cloud';
var BETA_RELAY = 'telebit.ppl.family';
var TELEBIT_RELAYS = [
DEFAULT_RELAY
, BETA_RELAY
];
var PRODUCTION_ACME = 'https://acme-v02.api.letsencrypt.org/directory';
var STAGING_ACME = 'https://acme-staging-v02.api.letsencrypt.org/directory';
var appData = {
config: {}
, status: {}
, init: {
teletos: true
, letos: true
, notifications: "important"
, relay: DEFAULT_RELAY
, telemetry: true
, acmeServer: PRODUCTION_ACME
}
, state: {}
, views: {
flash: {
error: ""
}
, section: {
loading: true
, setup: false
, advanced: false
, otp: false
, status: false
}
}
, newHttp: {}
};
var telebitState = {};
var appMethods = {
initialize: function () {
console.log("call initialize");
if (!appData.init.relay) {
appData.init.relay = DEFAULT_RELAY;
}
appData.init.relay = appData.init.relay.toLowerCase();
telebitState = { relay: appData.init.relay };
return Telebit.api.directory(telebitState).then(function (dir) {
if (!dir.api_host) {
window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service");
return;
}
telebitState.dir = dir;
// If it's one of the well-known relays
if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) {
return doConfigure();
} else {
changeState('advanced');
}
}).catch(function (err) {
console.error(err);
window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2)));
});
}
, advance: function () {
return doConfigure();
}
, productionAcme: function () {
console.log("prod acme:");
appData.init.acmeServer = PRODUCTION_ACME;
console.log(appData.init.acmeServer);
}
, stagingAcme: function () {
console.log("staging acme:");
appData.init.acmeServer = STAGING_ACME;
console.log(appData.init.acmeServer);
}
, defaultRelay: function () {
appData.init.relay = DEFAULT_RELAY;
}
, betaRelay: function () {
appData.init.relay = BETA_RELAY;
}
, enable: function () {
api.enable();
}
, disable: function () {
api.disable();
}
, ssh: function (port) {
// -1 to disable
// 0 is auto (22)
// 1-65536
api.ssh(port || 22);
}
, createShare: function (sub, domain, handler) {
if (sub) {
domain = sub + '.' + domain;
}
api.http({ name: domain, handler: handler, indexes: true });
appData.newHttp = {};
}
, createHost: function (sub, domain, handler) {
if (sub) {
domain = sub + '.' + domain;
}
api.http({ name: domain, handler: handler, 'x-forwarded-for': name });
appData.newHttp = {};
}
, changePortForward: function (domain, port) {
api.http({ name: domain.name, handler: port });
}
, deletePortForward: function (domain) {
api.http({ name: domain.name, handler: 'none' });
}
, changePathHost: function (domain, path) {
api.http({ name: domain.name, handler: path });
}
, deletePathHost: function (domain) {
api.http({ name: domain.name, handler: 'none' });
}
, changeState: changeState
};
var appStates = {
setup: function () {
appData.views.section = { setup: true };
}
, advanced: function () {
appData.views.section = { advanced: true };
}
, otp: function () {
appData.views.section = { otp: true };
}
, status: function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
return updateStatus().then(function () {
appData.views.section = { status: true, status_chooser: true };
return exitState;
});
}
};
appStates.status.share = function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
appData.views.section = { status: true, status_share: true };
return updateStatus().then(function () {
return exitState;
});
};
appStates.status.host = function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
appData.views.section = { status: true, status_host: true };
return updateStatus().then(function () {
return exitState;
});
};
appStates.status.access = function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
appData.views.section = { status: true, status_access: true };
return updateStatus().then(function () {
return exitState;
});
};
function updateStatus() {
return api.status().then(function (status) {
if (status.error) {
appData.views.flash.error = status.error.message || JSON.stringify(status.error, null, 2);
}
var wilddomains = [];
var rootdomains = [];
var subdomains = [];
var directories = [];
var portforwards = [];
var free = [];
appData.status = status;
if ('maybe' === status.ssh_requests_password) {
appData.status.ssh_active = false;
} else {
appData.status.ssh_active = true;
if ('yes' === status.ssh_requests_password) {
appData.status.ssh_insecure = true;
}
}
if ('yes' === status.ssh_password_authentication) {
appData.status.ssh_insecure = true;
}
if ('yes' === status.ssh_permit_root_login) {
appData.status.ssh_insecure = true;
}
// only update what's changed
if (appData.state.ssh !== appData.status.ssh) {
appData.state.ssh = appData.status.ssh;
}
if (appData.state.ssh_insecure !== appData.status.ssh_insecure) {
appData.state.ssh_insecure = appData.status.ssh_insecure;
}
if (appData.state.ssh_active !== appData.status.ssh_active) {
appData.state.ssh_active = appData.status.ssh_active;
}
Object.keys(appData.status.servernames).forEach(function (k) {
var s = appData.status.servernames[k];
s.name = k;
if (s.wildcard) { wilddomains.push(s); }
if (!s.sub && !s.wildcard) { rootdomains.push(s); }
if (s.sub) { subdomains.push(s); }
if (s.handler) {
if (s.handler.toString() === parseInt(s.handler, 10).toString()) {
s._port = s.handler;
portforwards.push(s);
} else {
s.path = s.handler;
directories.push(s);
}
} else {
free.push(s);
}
});
appData.status.portForwards = portforwards;
appData.status.pathHosting = directories;
appData.status.wildDomains = wilddomains;
appData.newHttp.name = (appData.status.wildDomains[0] || {}).name;
appData.state.ssh = (appData.status.ssh > 0) && appData.status.ssh || undefined;
});
}
function changeState(newstate) {
var newhash = '#/' + newstate + '/';
if (location.hash === newhash) {
if (!telebitState.firstState) {
telebitState.firstState = true;
setState();
}
}
location.hash = newhash;
}
/*globals Promise*/
window.addEventListener('hashchange', setState, false);
function setState(/*ev*/) {
//ev.oldURL
//ev.newURL
if (appData.exit) {
console.log('previous state exiting');
appData.exit.then(function (exit) {
if ('function' === typeof exit) {
exit();
}
});
}
var parts = location.hash.substr(1).replace(/^\//, '').replace(/\/$/, '').split('/').filter(Boolean);
var fn = appStates;
parts.forEach(function (s) {
console.log("state:", s);
fn = fn[s];
});
appData.exit = Promise.resolve(fn());
//appMethods.states[newstate]();
}
function msToHumanReadable(ms) {
var uptime = ms;
var uptimed = uptime / 1000;
var minute = 60;
var hour = 60 * minute;
var day = 24 * hour;
var days = 0;
var times = [];
while (uptimed > day) {
uptimed -= day;
days += 1;
}
times.push(days + " days ");
var hours = 0;
while (uptimed > hour) {
uptimed -= hour;
hours += 1;
}
times.push(hours.toString().padStart(2, "0") + " h ");
var minutes = 0;
while (uptimed > minute) {
uptimed -= minute;
minutes += 1;
}
times.push(minutes.toString().padStart(2, "0") + " m ");
var seconds = Math.round(uptimed);
times.push(seconds.toString().padStart(2, "0") + " s ");
return times.join('');
}
new Vue({
el: ".v-app"
, data: appData
, computed: {
statusProctime: function () {
return msToHumanReadable(this.status.proctime);
}
, statusRuntime: function () {
return msToHumanReadable(this.status.runtime);
}
, statusUptime: function () {
return msToHumanReadable(this.status.uptime);
}
}
, methods: appMethods
});
api.config().then(function (config) {
telebitState.config = config;
if (config.greenlock) {
appData.init.acmeServer = config.greenlock.server;
}
if (config.relay) {
appData.init.relay = config.relay;
}
if (config.email) {
appData.init.email = config.email;
}
if (config.agreeTos) {
appData.init.letos = config.agreeTos;
appData.init.teletos = config.agreeTos;
}
if (config._otp) {
appData.init.otp = config._otp;
}
telebitState.pollUrl = config._pollUrl || localStorage.getItem('poll_url');
if ((!config.token && !config._otp) || !config.relay || !config.email || !config.agreeTos) {
changeState('setup');
setState();
return;
}
if (!config.token && config._otp) {
changeState('otp');
setState();
// this will skip ahead as necessary
return Telebit.authorize(telebitState, showOtp).then(function () {
return changeState('status');
});
}
// TODO handle default state
changeState('status');
}).catch(function (err) {
appData.views.flash.error = err.message || JSON.stringify(err, null, 2);
});
window.api = api;
setTimeout(function () {
document.body.hidden = false;
}, 50);
}());

116
lib/admin/js/telebit-token.js

@ -0,0 +1,116 @@
;(function (exports) {
'use strict';
/* global Promise */
var PromiseA;
if ('undefined' !== typeof Promise) {
PromiseA = Promise;
} else {
throw new Error("no Promise implementation defined");
}
var common = exports.TELEBIT || require('./lib/common.js');
common.authorize = common.getToken = function getToken(state, showOtp) {
state.relay = state.config.relay;
// { _otp, config: {} }
return common.api.token(state, {
error: function (err) { console.error("[Error] common.api.token handlers.error: \n", err); return PromiseA.reject(err); }
, directory: function (dir) {
/*console.log('[directory] Telebit Relay Discovered:', dir);*/
state._apiDirectory = dir;
return PromiseA.resolve();
}
, tunnelUrl: function (tunnelUrl) {
//console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
state.wss = tunnelUrl;
return PromiseA.resolve();
}
, requested: function (authReq, pollUrl) {
console.log("[requested] Pairing Requested");
state._otp = state._otp = authReq.otp;
if (!state.config.token && state._can_pair) {
console.info("0000".replace(/0000/g, state._otp));
showOtp(authReq.otp, pollUrl);
}
return PromiseA.resolve();
}
, connect: function (pretoken) {
console.log("[connect] Enabling Pairing Locally...");
state.config.pretoken = pretoken;
state._connecting = true;
// This will only be saved to the session
state.config._otp = state._otp;
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () {
console.info("waiting...");
return PromiseA.resolve();
}).catch(function (err) {
state._error = err;
console.error("Error while initializing config [connect]:");
console.error(err);
return PromiseA.reject(err);
});
}
, offer: function (token) {
//console.log("[offer] Pairing Enabled by Relay");
state.token = token;
state.config.token = token;
if (state._error) {
return;
}
state._connecting = true;
try {
//require('jsonwebtoken').decode(token);
token = token.split('.');
token[0] = token[0].replace(/_/g, '/').replace(/-/g, '+');
while (token[0].length % 4) { token[0] += '='; }
btoa(token[0]);
token[1] = token[1].replace(/_/g, '/').replace(/-/g, '+');
while (token[1].length % 4) { token[1] += '='; }
btoa(token[1]);
//console.log(require('jsonwebtoken').decode(token));
} catch(e) {
console.warn("[warning] could not decode token");
}
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () {
//console.log("Pairing Enabled Locally");
return PromiseA.resolve();
}).catch(function (err) {
state._error = err;
console.error("Error while initializing config [offer]:");
console.error(err);
return PromiseA.reject(err);
});
}
, granted: function (/*_*/) { /*console.log("[grant] Pairing complete!");*/ return PromiseA.resolve(); }
, end: function () {
return common.reqLocalAsync({ url: '/api/enable', method: 'POST', body: [], json: true }).then(function () {
console.info("Success");
// workaround for https://github.com/nodejs/node/issues/21319
if (state._useTty) {
setTimeout(function () {
console.info("Some fun things to try first:\n");
console.info(" ~/telebit http ~/public");
console.info(" ~/telebit tcp 5050");
console.info(" ~/telebit ssh auto");
console.info();
console.info("Press any key to continue...");
console.info();
process.exit(0);
}, 0.5 * 1000);
return;
}
// end workaround
//parseCli(state);
});
}
});
};
}('undefined' === typeof module ? window : module.exports));

338
lib/admin/js/telebit.js

@ -0,0 +1,338 @@
;(function (exports) {
'use strict';
var common = exports.TELEBIT = {};
common.debug = true;
/* global Promise */
var PromiseA;
if ('undefined' !== typeof Promise) {
PromiseA = Promise;
} else {
throw new Error("no Promise implementation defined");
}
/*globals AbortController*/
if ('undefined' !== typeof fetch) {
common.requestAsync = function (opts) {
// funnel requests through the local server
// (avoid CORS, for now)
var relayOpts = {
url: '/api/relay'
, method: 'POST'
, headers: {
'Content-Type': 'application/json'
, 'Accepts': 'application/json'
}
, body: JSON.stringify(opts)
};
var controller = new AbortController();
var tok = setTimeout(function () {
controller.abort();
}, 4000);
if (!relayOpts) {
relayOpts = {};
}
relayOpts.signal = controller.signal;
return window.fetch(relayOpts.url, relayOpts).then(function (resp) {
clearTimeout(tok);
return resp.json().then(function (json) {
if (json.error) {
return PromiseA.reject(new Error(json.error && json.error.message || JSON.stringify(json.error)));
}
return json;
});
});
};
common.reqLocalAsync = function (opts) {
if (!opts) { opts = {}; }
if (opts.json && true !== opts.json) {
opts.body = opts.json;
opts.json = true;
}
if (opts.json) {
if (!opts.headers) { opts.headers = {}; }
if (opts.body) {
opts.headers['Content-Type'] = 'application/json';
if ('string' !== typeof opts.body) {
opts.body = JSON.stringify(opts.body);
}
} else {
opts.headers.Accepts = 'application/json';
}
}
var controller = new AbortController();
var tok = setTimeout(function () {
controller.abort();
}, 4000);
opts.signal = controller.signal;
return window.fetch(opts.url, opts).then(function (resp) {
clearTimeout(tok);
return resp.json().then(function (json) {
var headers = {};
resp.headers.forEach(function (k, v) {
headers[k] = v;
});
return { statusCode: resp.status, headers: headers, body: json };
});
});
};
} else {
common.requestAsync = require('util').promisify(require('@coolaj86/urequest'));
common.reqLocalAsync = require('util').promisify(require('@coolaj86/urequest'));
}
common.parseUrl = function (hostname) {
// add scheme, if missing
if (!/:\/\//.test(hostname)) {
hostname = 'https://' + hostname;
}
var location = new URL(hostname);
hostname = location.hostname + (location.port ? ':' + location.port : '');
hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
return hostname;
};
common.parseHostname = function (hostname) {
var location = {};
try {
location = new URL(hostname);
} catch(e) {
// ignore
}
if (!location.protocol || /\./.test(location.protocol)) {
hostname = 'https://' + hostname;
location = new URL(hostname);
}
//hostname = location.hostname + (location.port ? ':' + location.port : '');
//hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
return location.hostname;
};
common.apiDirectory = '_apis/telebit.cloud/index.json';
common.otp = function getOtp() {
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
};
common.signToken = function (state) {
var JWT = require('./jwt.js');
var tokenData = {
domains: Object.keys(state.config.servernames || {}).filter(function (name) {
return /\./.test(name);
})
, ports: Object.keys(state.config.ports || {}).filter(function (port) {
port = parseInt(port, 10);
return port > 0 && port <= 65535;
})
, aud: state._relayUrl
, iss: Math.round(Date.now() / 1000)
};
return JWT.sign(tokenData, state.config.secret);
};
common.promiseTimeout = function (ms) {
var tok;
var p = new PromiseA(function (resolve) {
tok = setTimeout(function () {
resolve();
}, ms);
});
p.cancel = function () {
clearTimeout(tok);
};
return p;
};
common.api = {};
common.api.directory = function (state) {
console.log('[DEBUG] state:');
console.log(state);
state._relayUrl = common.parseUrl(state.relay);
if (!state._relays) { state._relays = {}; }
if (state._relays[state._relayUrl]) {
return PromiseA.resolve(state._relays[state._relayUrl]);
}
return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) {
var dir = resp.body;
state._relays[state._relayUrl] = dir;
return dir;
});
};
common.api._parseWss = function (state, dir) {
if (!dir || !dir.api_host) {
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
}
state._relayHostname = common.parseHostname(state.relay);
return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname;
};
common.api.wss = function (state) {
return common.api.directory(state).then(function (dir) {
return common.api._parseWss(state, dir);
});
};
common.api.token = function (state, handlers) {
var firstReady = true;
function pollStatus(req) {
if (common.debug) { console.log('[debug] pollStatus called'); }
if (common.debug) { console.log(req); }
return common.requestAsync(req).then(function checkLocation(resp) {
var body = resp.body;
if (common.debug) { console.log('[debug] checkLocation'); }
if (common.debug) { console.log(body); }
// pending, try again
if ('pending' === body.status && resp.headers.location) {
if (common.debug) { console.log('[debug] pending'); }
return common.promiseTimeout(2 * 1000).then(function () {
return pollStatus({ url: resp.headers.location, json: true });
});
} else if ('ready' === body.status) {
if (common.debug) { console.log('[debug] ready'); }
if (firstReady) {
if (common.debug) { console.log('[debug] first ready'); }
firstReady = false;
// falls through on purpose
PromiseA.resolve(handlers.offer(body.access_token)).then(function () {
/*ignore*/
});
}
return common.promiseTimeout(2 * 1000).then(function () {
return pollStatus(req);
});
} else if ('complete' === body.status) {
if (common.debug) { console.log('[debug] complete'); }
return PromiseA.resolve(handlers.granted(null)).then(function () {
return PromiseA.resolve(handlers.end(null)).then(function () {});
});
} else {
if (common.debug) { console.log('[debug] bad status'); }
var err = new Error("Bad State:" + body.status);
err._request = req;
return PromiseA.reject(err);
}
}).catch(function (err) {
if (common.debug) { console.log('[debug] pollStatus error'); }
err._request = req;
err._hint = '[telebitd.js] pair request';
return PromiseA.resolve(handlers.error(err)).then(function () {});
});
}
// directory, requested, connect, tunnelUrl, offer, granted, end
function requestAuth(dir) {
if (common.debug) { console.log('[debug] after dir'); }
state.wss = common.api._parseWss(state, dir);
return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(function () {
if (common.debug) { console.log('[debug] after tunnelUrl'); }
if (state.config.secret /* && !state.config.token */) {
// TODO make token here in the browser
//state.config._token = common.signToken(state);
}
state.token = state.token || state.config.token || state.config._token;
if (state.token) {
if (common.debug) { console.log('[debug] token via token or secret'); }
// { token, pretoken }
return PromiseA.resolve(handlers.connect(state.token)).then(function () {
return PromiseA.resolve(handlers.end(null));
});
}
if (!dir.pair_request) {
if (common.debug) { console.log('[debug] no dir, connect'); }
return PromiseA.resolve(handlers.error(new Error("No token found or generated, and no pair_request api found.")));
}
// TODO sign token with own private key, including public key and thumbprint
// (much like ACME JOSE account)
// TODO handle agree
var otp = state._otp; // common.otp();
var authReq = {
subject: state.config.email
, subject_scheme: 'mailto'
// TODO create domains list earlier
, scope: (state.config._servernames || Object.keys(state.config.servernames || {}))
.concat(state.config._ports || Object.keys(state.config.ports || {})).join(',')
, otp: otp
// TODO make call to daemon for this info beforehand
/*
, hostname: os.hostname()
// Used for User-Agent
, os_type: os.type()
, os_platform: os.platform()
, os_release: os.release()
, os_arch: os.arch()
*/
};
var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname));
console.log('pairRequestUrl:', pairRequestUrl);
//console.log('pairRequestUrl:', JSON.stringify(pairRequestUrl.toJSON()));
var req = {
// WHATWG URL defines .toJSON() but, of course, it's not implemented
// because... why would we implement JavaScript objects in the DOM
// when we can have perfectly incompatible non-JS objects?
url: {
host: pairRequestUrl.host
, hostname: pairRequestUrl.hostname
, href: pairRequestUrl.href
, pathname: pairRequestUrl.pathname
// because why wouldn't node require 'path' on a json object and accept 'pathname' on a URL object...
// https://twitter.com/coolaj86/status/1053947919890403328
, path: pairRequestUrl.pathname
, port: pairRequestUrl.port || null
, protocol: pairRequestUrl.protocol
, search: pairRequestUrl.search || null
}
, method: dir.pair_request.method
, json: authReq
};
return common.requestAsync(req).then(function doFirst(resp) {
var body = resp.body;
if (common.debug) { console.log('[debug] first req'); }
if (!body.access_token && !body.jwt) {
return PromiseA.reject(new Error("something wrong with pre-authorization request"));
}
return PromiseA.resolve(handlers.requested(authReq, resp.headers.location)).then(function () {
return PromiseA.resolve(handlers.connect(body.access_token || body.jwt)).then(function () {
var err;
if (!resp.headers.location) {
err = new Error("bad authentication request response");
err._resp = resp.toJSON && resp.toJSON();
return PromiseA.resolve(handlers.error(err)).then(function () {});
}
return common.promiseTimeout(2 * 1000).then(function () {
return pollStatus({ url: resp.headers.location, json: true });
});
});
});
}).catch(function (err) {
if (common.debug) { console.log('[debug] gotoFirst error'); }
err._request = req;
err._hint = '[telebitd.js] pair request';
return PromiseA.resolve(handlers.error(err)).then(function () {});
});
});
}
if (state.pollUrl) {
return pollStatus({ url: state.pollUrl, json: true });
}
// backwards compat (TODO verify we can remove this)
var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }';
return common.api.directory(state).then(function (dir) {
console.log('[debug] [directory]', dir);
if (!dir.api_host) { dir = JSON.parse(failoverDir); }
return dir;
}).catch(function (err) {
console.warn('[warn] [directory] fetch fail, using failover');
console.warn(err);
return JSON.parse(failoverDir);
}).then(function (dir) {
return PromiseA.resolve(handlers.directory(dir)).then(function () {
console.log('[debug] [directory]', dir);
return requestAuth(dir);
});
});
};
}('undefined' !== typeof module ? module.exports : window));

10947
lib/admin/js/vue.js

File diff suppressed because it is too large

4
lib/cli-common.js

@ -118,7 +118,7 @@ common.otp = function getOtp() {
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
};
common.signToken = function (state) {
var jwt = require('jsonwebtoken');
var JWT = require('./jwt.js');
var tokenData = {
domains: Object.keys(state.config.servernames || {}).filter(function (name) {
return /\./.test(name);
@ -131,7 +131,7 @@ common.signToken = function (state) {
, iss: Math.round(Date.now() / 1000)
};
return jwt.sign(tokenData, state.config.secret);
return JWT.sign(tokenData, state.config.secret);
};
common.api = {};
common.api.directory = function (state, next) {

10
lib/remote.js → lib/daemon/index.js

@ -28,6 +28,7 @@ function TelebitRemote(state) {
EventEmitter.call(this);
var me = this;
var priv = {};
var path = require('path');
//var defaultHttpTimeout = (2 * 60);
//var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
@ -39,8 +40,9 @@ function TelebitRemote(state) {
priv.tokens = [];
var auth;
if(!state.sortingHat) {
state.sortingHat = "./sorting-hat.js";
state.sortingHat = path.join(__dirname, '../sorting-hat.js');
}
state._connectionHandler = require(state.sortingHat);
if (state.token) {
if ('undefined' === state.token) {
throw new Error("passed string 'undefined' as token");
@ -349,7 +351,7 @@ function TelebitRemote(state) {
// TODO use readable streams instead
wstunneler._socket.pause();
require(state.sortingHat).assign(state, tun, function (err, conn) {
state._connectionHandler.assign(state, tun, function (err, conn) {
if (err) {
err.message = err.message.replace(/:tun_id/, tun._id);
packerHandlers._onConnectError(cid, tun, err);
@ -472,12 +474,12 @@ function TelebitRemote(state) {
priv.timeoutId = null;
var machine = Packer.create(packerHandlers);
console.info("[telebit:lib/remote.js] [connect] '" + (state.wss || state.relay) + "'");
console.info("[telebit:lib/daemon.js] [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("[telebit:lib/remote.js] [open] connected to '" + (state.wss || state.relay) + "'");
console.info("[telebit:lib/daemon.js] [open] connected to '" + (state.wss || state.relay) + "'");
me.emit('connect');
priv.refreshTimeout();
priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout);

102
lib/eggspress.js

@ -0,0 +1,102 @@
'use strict';
function eggSend(obj) {
/*jslint validthis: true*/
var me = this;
if (!me.getHeader('content-type')) {
me.setHeader('Content-Type', 'application/json');
}
me.end(JSON.stringify(obj));
}
module.exports = function eggspress() {
//var patternsMap = {};
var allPatterns = [];
var app = function (req, res) {
var patterns = allPatterns.slice(0).reverse();
function next(err) {
if (err) {
req.end(err.message);
return;
}
var todo = patterns.pop();
if (!todo) {
console.log('[eggspress] Did not match any patterns', req.url);
require('finalhandler')(req, res)();
return;
}
// '', GET, POST, DELETE
if (todo[2] && req.method.toLowerCase() !== todo[2]) {
//console.log("[eggspress] HTTP method doesn't match", req.url);
next();
return;
}
var urlstr = (req.url.replace(/\/$/, '') + '/');
if (!urlstr.match(todo[0])) {
//console.log("[eggspress] pattern doesn't match", todo[0], req.url);
next();
return;
} else if ('string' === typeof todo[0] && 0 !== urlstr.match(todo[0]).index) {
//console.log("[eggspress] string pattern is not the start", todo[0], req.url);
next();
return;
}
function fail(e) {
console.error("[eggspress] error", todo[2], todo[0], req.url);
console.error(e);
// TODO make a nice error message
res.end(e.message);
}
try {
console.log("[eggspress] matched pattern", todo[0], req.url);
var p = todo[1](req, res, next);
if (p && p.catch) {
p.catch(fail);
}
} catch(e) {
fail(e);
return;
}
}
res.send = eggSend;
next();
};
app.use = function (pattern, fn) {
return app._use('', pattern, fn);
};
[ 'HEAD', 'GET', 'POST', 'DELETE' ].forEach(function (method) {
app[method.toLowerCase()] = function (pattern, fn) {
return app._use(method, pattern, fn);
};
});
app.post = function (pattern, fn) {
return app._use('POST', pattern, fn);
};
app._use = function (method, pattern, fn) {
// always end in a slash, for now
if ('string' === typeof pattern) {
pattern = pattern.replace(/\/$/, '') + '/';
}
/*
if (!patternsMap[pattern]) {
patternsMap[pattern] = [];
}
patternsMap[pattern].push(fn);
patterns = Object.keys(patternsMap).sort(function (a, b) {
return b.length - a.length;
});
*/
allPatterns.push([pattern, fn, method.toLowerCase()]);
return app;
};
return app;
};

8
lib/en-us.toml

@ -476,5 +476,13 @@ By using Telebit you agree to:
Enter your email to agree and login/create your account:
"
fail_relay_check = "===================
WARNING
===================
[{{status_code}}] '{{url}}'
This server does not describe a current telebit version (but it may still work).
"
[daemon]
version = "telebit daemon v{version}"

19
lib/jwt-test.js

@ -0,0 +1,19 @@
'use strict';
var crypto = require('crypto');
var FAT = require('jsonwebtoken');
var JWT = require('./jwt.js');
var key = "justanothersecretsecret";
var keyid = crypto.createHash('sha256').update(key).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
var tok1 = FAT.sign({ foo: "hello" }, key, { keyid: keyid });
var tok2 = JWT.sign({ foo: "hello" }, key);
if (tok1 !== tok2) {
console.error(JWT.decode(tok1));
console.error(JWT.decode(tok2));
throw new Error("our jwt doesn't match auth0/jsonwebtoken");
}
console.info('Pass');

43
lib/jwt.js

@ -0,0 +1,43 @@
'use strict';
var crypto = require('crypto');
var JWT = module.exports;
JWT.decode = function (jwt) {
var parts;
try {
parts = jwt.split('.');
return {
header: JSON.parse(Buffer.from(parts[0], 'base64'))
, payload: JSON.parse(Buffer.from(parts[1], 'base64'))
, signature: parts[2] //Buffer.from(parts[2], 'base64')
};
} catch(e) {
throw new Error("JWT Parse Error: could not split, base64 decode, and JSON.parse token " + jwt);
}
};
JWT.verify = function (jwt) {
var decoded = JWT.decode(jwt);
throw new Error("not implemented yet");
};
function base64ToUrlSafe(str) {
return str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
;
}
JWT.sign = function (claims, key) {
if (!claims.iat && false !== claims.iat) {
claims.iat = Math.round(Date.now()/1000);
}
var thumb = base64ToUrlSafe(crypto.createHash('sha256').update(key).digest('base64'));
var protect = base64ToUrlSafe(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: thumb })).toString('base64'));
var payload = base64ToUrlSafe(Buffer.from(JSON.stringify(claims)).toString('base64'));
var signature = base64ToUrlSafe(crypto.createHmac('sha256', key).update(protect + '.' + payload).digest('base64'));
return protect + '.' + payload + '.' + signature;
};

37
lib/keystore-fallback.js

@ -0,0 +1,37 @@
'use strict';
/*global Promise*/
var fs = require('fs').promises;
var path = require('path');
module.exports.create = function (opts) {
var keyext = '.key';
return {
getPassword: function (service, name) {
var f = path.join(opts.configDir, name + keyext);
return fs.readFile(f, 'utf8').catch(function (err) {
if ('ENOEXIST' === err.code) {
return;
}
});
}
, setPassword: function (service, name, key) {
var f = path.join(opts.configDir, name + keyext);
return fs.writeFile(f, key, 'utf8');
}
, deletePassword: function (service, name) {
var f = path.join(opts.configDir, name + keyext);
return fs.unlink(f);
}
, findCredentials: function (/*service*/) {
return fs.readDir(opts.configDir).then(function (nodes) {
return Promise.all(nodes.filter(function (node) {
return keyext === node.slice(-4);
}).map(function (node) {
return fs.readFile(path.join(opts.configDir, node + keyext));
}));
});
}
, insecure: true
};
};

22
lib/keystore-test.js

@ -0,0 +1,22 @@
'use strict';
var keystore = require('./keystore.js').create({
configDir: require('path').join(require('os').homedir(), '.local/telebit/')
});
var name = "testy-mctestface-1";
return keystore.get(name).then(function (jwk) {
console.log("get1", typeof jwk, jwk);
if (!jwk || !jwk.kty) {
return require('keypairs').generate().then(function (jwk) {
var json = JSON.stringify(jwk.private);
return keystore.set(name, json).then(function () {
return keystore.get(name).then(function (val2) {
console.log("get2", val2);
});
}).catch(function (err) {
console.log('badness', err);
});
});
}
return jwk;
});

49
lib/keystore.js

@ -0,0 +1,49 @@
'use strict';
module.exports.create = function (opts) {
var service = opts.name || "Telebit";
var keytar;
try {
keytar = require('keytar');
// TODO test that long "passwords" (JWTs and JWKs) can be stored in all OSes
} catch(e) {
console.warn("Could not load native key management. Keys will be stored in plain text.");
keytar = require('./keystore-fallback.js').create(opts);
keytar.insecure = true;
}
return {
get: function (name) {
return keytar.getPassword(service, name).then(maybeParse);
}
, set: function (name, value) {
return keytar.setPassword(service, name, maybeStringify(value));
}
, delete: function (name) {
return keytar.deletePassword(service, name);
}
, all: function () {
return keytar.findCredentials(service).then(function (list) {
return list.map(function (el) {
el.password = maybeParse(el.password);
return el;
});
});
}
, insecure: keytar.insecure
};
};
function maybeParse(str) {
if (str && '{' === str[0]) {
return JSON.parse(str);
}
return str;
}
function maybeStringify(obj) {
if ('string' !== typeof obj && 'object' === typeof obj) {
return JSON.stringify(obj);
}
return obj;
}

175
lib/rc/index.js

@ -0,0 +1,175 @@
'use strict';
var os = require('os');
var path = require('path');
var http = require('http');
var keypairs = require('keypairs');
var common = require('../cli-common.js');
/*
function packConfig(config) {
return Object.keys(config).map(function (key) {
var val = config[key];
if ('undefined' === val) {
throw new Error("'undefined' used as a string value");
}
if ('undefined' === typeof val) {
//console.warn('[DEBUG]', key, 'is present but undefined');
return;
}
if (val && 'object' === typeof val && !Array.isArray(val)) {
val = JSON.stringify(val);
}
return key + ':' + val; // converts arrays to strings with ,
});
}
*/
module.exports.create = function (state) {
common._init(
// make a default working dir and log dir
state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit')
, (state._clientConfig.root && path.join(state._clientConfig.root, 'etc'))
|| path.resolve(common.DEFAULT_CONFIG_PATH, '..')
);
state._ipc = common.pipename(state._clientConfig, true);
function makeResponder(service, resp, fn) {
var body = '';
function finish() {
var err;
if (200 !== resp.statusCode) {
err = new Error(body || ('get ' + service + ' failed'));
err.statusCode = resp.statusCode;
err.code = "E_REQUEST";
}
if (body) {
try {
body = JSON.parse(body);
} catch(e) {
console.error('Error:', err);
// ignore
}
}
fn(err, body);
}
if (!resp.headers['content-length'] && !resp.headers['content-type']) {
finish();
return;
}
// TODO use readable
resp.on('data', function (chunk) {
body += chunk.toString();
});
resp.on('end', finish);
}
var RC = {};
RC.resolve = function (pathstr) {
// TODO use real hostname and return reqOpts rather than string?
return 'http://localhost:' + (RC.port({}).port||'1').toString() + '/' + pathstr.replace(/^\//, '');
};
RC.port = function (reqOpts) {
var fs = require('fs');
var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port');
if (fs.existsSync(portFile)) {
reqOpts.host = 'localhost';
reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
if (!state.ipc) {
state.ipc = {};
}
state.ipc.type = 'port';
state.ipc.path = path.dirname(state._ipc.path);
state.ipc.port = reqOpts.port;
} else {
reqOpts.socketPath = state._ipc.path;
}
return reqOpts;
};
RC.createErrorHandler = function (replay, opts, cb) {
return function (err) {
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
// ECONNREFUSED - leftover socket just needs to be restarted
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
if (opts._taketwo) {
cb(err);
return;
}
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
if (err) { cb(err); return; }
opts._taketwo = true;
setTimeout(function () {
replay(opts, cb);
}, 2500);
});
return;
}
cb(err);
};
};
RC.request = function request(opts, fn) {
if (!opts) { opts = {}; }
var service = opts.service || 'config';
/*
var args = opts.data;
if (args && 'control' === service) {
args = packConfig(args);
}
var json = JSON.stringify(opts.data);
*/
var url = '/rpc/' + service;
/*
if (json) {
url += ('?_body=' + encodeURIComponent(json));
}
*/
var method = opts.method || (opts.data && 'POST') || 'GET';
var reqOpts = {
method: method
, path: url
};
reqOpts = RC.port(reqOpts);
var req = http.request(reqOpts, function (resp) {
makeResponder(service, resp, fn);
});
req.on('error', RC.createErrorHandler(RC.request, opts, fn));
// Simple GET
if ('POST' !== method || !opts.data) {
return keypairs.signJwt({
jwk: state.key
, claims: { iss: false, exp: Math.round(Date.now()/1000) + (15 * 60) }
//TODO , exp: '15m'
}).then(function (jwt) {
req.setHeader("authorization", 'bearer ' + jwt);
req.end();
});
}
return keypairs.signJws({
jwk: state.key
, protected: {
// alg will be filled out automatically
jwk: state.pub
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
// TODO make localhost exceptional
, url: RC.resolve(reqOpts.path)
}
, payload: JSON.stringify(opts.data)
}).then(function (jws) {
req.setHeader("Content-Type", 'application/jose+json');
req.write(JSON.stringify(jws));
req.end();
});
};
return RC;
};

76
lib/ssh.js

@ -0,0 +1,76 @@
'use strict';
/*global Promise*/
var PromiseA = Promise;
var crypto = require('crypto');
var util = require('util');
var readFile = util.promisify(require('fs').readFile);
var exec = require('child_process').exec;
function sshAllowsPassword(user) {
// SSH on Windows is a thing now (beta 2015, standard 2018)
// https://stackoverflow.com/questions/313111/is-there-a-dev-null-on-windows
var nullfile = '/dev/null';
if (/^win/i.test(process.platform)) {
nullfile = 'NUL';
}
var args = [
'ssh', '-v', '-n'
, '-o', 'Batchmode=yes'
, '-o', 'StrictHostKeyChecking=no'
, '-o', 'UserKnownHostsFile=' + nullfile
, user + '@localhost'
, '| true'
];
return new PromiseA(function (resolve) {
// not using promisify because all 3 arguments convey information
exec(args.join(' '), function (err, stdout, stderr) {
stdout = (stdout||'').toString('utf8');
stderr = (stderr||'').toString('utf8');
if (/\bpassword\b/.test(stdout) || /\bpassword\b/.test(stderr)) {
resolve('yes');
return;
}
if (/\bAuthentications\b/.test(stdout) || /\bAuthentications\b/.test(stderr)) {
resolve('no');
return;
}
resolve('maybe');
});
});
}
module.exports.checkSecurity = function () {
var conf = {};
var noRootPasswordRe = /(?:^|[\r\n]+)\s*PermitRootLogin\s+(prohibit-password|without-password|no)\s*/i;
var noPasswordRe = /(?:^|[\r\n]+)\s*PasswordAuthentication\s+(no)\s*/i;
var sshdConf = '/etc/ssh/sshd_config';
if (/^win/i.test(process.platform)) {
// TODO use %PROGRAMDATA%\ssh\sshd_config
sshdConf = 'C:\\ProgramData\\ssh\\sshd_config';
}
return readFile(sshdConf, null).then(function (sshd) {
sshd = sshd.toString('utf8');
var match;
match = sshd.match(noRootPasswordRe);
conf.permit_root_login = match ? match[1] : 'yes';
match = sshd.match(noPasswordRe);
conf.password_authentication = match ? match[1] : 'yes';
}).catch(function () {
// ignore error as that might not be the correct sshd_config location
}).then(function () {
var doesntExist = crypto.randomBytes(16).toString('hex');
return sshAllowsPassword(doesntExist).then(function (maybe) {
conf.requests_password = maybe;
});
}).then(function () {
return conf;
});
};
if (require.main === module) {
module.exports.checkSecurity().then(function (conf) {
console.log(conf);
return conf;
});
}

1124
package-lock.json

File diff suppressed because it is too large

12
package.json

@ -1,8 +1,8 @@
{
"name": "telebit",
"version": "0.20.8",
"version": "0.21.0-wip.1",
"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",
"main": "lib/daemon/index.js",
"files": [
"bin",
"lib",
@ -55,9 +55,10 @@
"dependencies": {
"@coolaj86/urequest": "^1.3.5",
"finalhandler": "^1.1.1",
"greenlock": "^2.3.1",
"greenlock": "^2.6.7",
"js-yaml": "^3.11.0",
"jsonwebtoken": "^7.1.9",
"keyfetch": "^1.1.8",
"keypairs": "^1.2.14",
"mkdirp": "^0.5.1",
"proxy-packer": "^2.0.2",
"ps-list": "^5.0.0",
@ -72,6 +73,9 @@
"toml": "^0.4.1",
"ws": "^6.0.0"
},
"optionalDependencies": {
"keytar": "^4.4.1"
},
"trulyOptionalDependencies": {
"bluebird": "^3.5.1"
},

34
usr/share/install-launcher.js

@ -11,7 +11,7 @@ Launcher._killAll = function (fn) {
var psList = require('ps-list');
psList().then(function (procs) {
procs.forEach(function (proc) {
if ('node' === proc.name && /\btelebitd\b/i.test(proc.cmd)) {
if ('node' === proc.name && /\btelebit(d| daemon)\b/i.test(proc.cmd)) {
console.log(proc);
process.kill(proc.pid);
return true;
@ -45,37 +45,7 @@ Launcher._detect = function (things, fn) {
}
}
// could have used "command-exists" but I'm trying to stay low-dependency
// os.platform(), os.type()
if (!/^win/i.test(os.platform())) {
if (/^darwin/i.test(os.platform())) {
exec('command -v launchctl', things._execOpts, function (err, stdout, stderr) {
err = Launcher._getError(err, stderr);
fn(err, 'launchctl');
});
} else {
exec('command -v systemctl', things._execOpts, function (err, stdout, stderr) {
err = Launcher._getError(err, stderr);
fn(err, 'systemctl');
});
}
} else {
// https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
// wininit? regedit? SCM?
// REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
// https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
// https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
// utils.elevate
// https://github.com/CatalystCode/windows-registry-node
exec('where reg.exe', things._execOpts, function (err, stdout, stderr) {
//console.log((stdout||'').trim());
if (stderr) {
console.error(stderr);
}
fn(err, 'reg.exe');
});
}
require('./which.js').launcher(things._execOpts, fn);
};
Launcher.install = function (things, fn) {
if (!fn) { fn = function (err) { if (err) { console.error(err); } }; }

63
usr/share/which.js

@ -0,0 +1,63 @@
'use strict';
var os = require('os');
var exec = require('child_process').exec;
var which = module.exports;
which._getError = function getError(err, stderr) {
if (err) { return err; }
if (stderr) {
err = new Error(stderr);
err.code = 'EWHICH';
return err;
}
};
module.exports.which = function (cmd, execOpts, fn) {
return module.exports._which({
mac: cmd
, linux: cmd
, win: cmd
}, execOpts, fn);
};
module.exports.launcher = function (execOpts, fn) {
return module.exports._which({
mac: 'launchctl'
, linux: 'systemctl'
, win: 'reg.exe'
}, execOpts, fn);
};
module.exports._which = function (progs, execOpts, fn) {
// could have used "command-exists" but I'm trying to stay low-dependency
// os.platform(), os.type()
if (!/^win/i.test(os.platform())) {
if (/^darwin/i.test(os.platform())) {
exec('command -v ' + progs.mac, execOpts, function (err, stdout, stderr) {
err = which._getError(err, stderr);
fn(err, progs.mac);
});
} else {
exec('command -v ' + progs.linux, execOpts, function (err, stdout, stderr) {
err = which._getError(err, stderr);
fn(err, progs.linux);
});
}
} else {
// https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
// wininit? regedit? SCM?
// REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
// https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
// https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
// utils.elevate
// https://github.com/CatalystCode/windows-registry-node
exec('where ' + progs.win, execOpts, function (err, stdout, stderr) {
//console.log((stdout||'').trim());
if (stderr) {
console.error(stderr);
}
fn(err, progs.win);
});
}
};
Loading…
Cancel
Save