Compare commits

..

13 Commits
wip ... stable

Author SHA1 Message Date
f0049c7f06 better param parsing 2019-05-28 17:07:45 -06:00
b5d57817cf fix some typos, move some phrases to the i18n toml file 2019-05-18 13:52:51 -06:00
1e3f7f671d use Root links (new branding) 2019-05-18 13:37:49 -06:00
00f3b3ab45 more descriptive wait message 2019-05-18 13:24:19 -06:00
fountainheadllc
19a42a596c Update 'lib/html/index.html' 2019-05-17 16:09:23 +00:00
fountainheadllc
de2290dd3e Update 'lib/html/index.html' 2019-05-17 03:14:11 +00:00
fountainheadllc
7db8a7a4ae Update 'lib/html/index.html'
Rachel's edits to index.html to make information easier to understand.
2019-05-15 00:28:15 +00:00
ceddf444b0 update node version to v10.13 2019-05-11 18:22:20 -06:00
76ec7eb066 typo fix conifg -> config 2019-01-17 06:31:32 +00:00
RubenVinke
05dab9a52c 'README.md' updaten 2019-01-16 23:03:09 +00:00
20321b2fbe continue when systemd --user fails 2018-11-20 17:01:57 -07:00
8bf4bfc7c0 typo fix apt-install -> apt-get install 2018-11-20 16:59:32 -07:00
4f0db8bc9c continue when systemd --user fails 2018-11-20 16:57:44 -07:00
28 changed files with 818 additions and 15252 deletions

View File

@ -1,10 +1,7 @@
# Telebit™ Remote
# Telebit™ Remote | a [Root](https://rootprojects.org) project
The T-Rex Long-Arm of the Internet
Because friends don't let friends localhost™
<small>because friends don't let friends localhost</small>
| Sponsored by [ppl](https://ppl.family)
| **Telebit Remote**
| [Telebit Relay](https://git.coolaj86.com/coolaj86/telebit-relay.js)
| [sclient](https://telebit.cloud/sclient)
@ -123,8 +120,8 @@ Windows & Node.js
1. Install [node.js](https://nodejs.org)
2. Open _Node.js_
2. Run the command `npm install -g telebit`
2. Copy the example daemon conifg to your user folder `.config/telebit/telebitd.yml` (such as `/Users/John/.config/telebit/telebitd.yml`)
2. Copy the example remote conifg to your user folder `.config/telebit/telebit.yml` (such as `/Users/John/.config/telebit/telebit.yml`)
2. Copy the example daemon config to your user folder `.config/telebit/telebitd.yml` (such as `/Users/John/.config/telebit/telebitd.yml`)
2. Copy the example remote config to your user folder `.config/telebit/telebit.yml` (such as `/Users/John/.config/telebit/telebit.yml`)
2. Change the email address
2. Run `npx telebit init` and follow the instructions
2. Run `npx telebit list`
@ -526,7 +523,7 @@ rm -rf ~/.config/telebit ~/.local/share/telebit
Browser Library
=======
This is implemented with websockets, so browser compatibility is a hopeful future outcome. Would love help.
This is implemented with websockets, so you should be able to
LICENSE
=======

View File

@ -8,13 +8,11 @@ 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');
@ -25,12 +23,8 @@ 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');
@ -45,30 +39,6 @@ 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) {
@ -87,9 +57,7 @@ function help() {
var verstr = [ pkg.name + ' remote v' + pkg.version ];
if (!confpath) {
state.configDir = defaultConfPath;
state.configFile = defaultConfFile;
confpath = state.configFile;
confpath = path.join(os.homedir(), '.config/telebit/telebit.yml');
verstr.push('(--config \'' + confpath.replace(new RegExp('^' + os.homedir()), '~') + '\')');
}
@ -155,10 +123,13 @@ function askForConfig(state, mainCb) {
return;
}
if (200 !== resp.statusCode || (Buffer.isBuffer(body) || 'object' !== typeof body) || !body.api_host) {
console.warn(TPLS.remote.setup.fail_relay_check
.replace(/{{\s*status_code\s*}}/, resp.statusCode)
.replace(/{{\s*url\s*}}/, urlstr)
);
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(body);
} else if (body && body.pair_request) {
state._can_pair = true;
@ -187,7 +158,7 @@ function askForConfig(state, mainCb) {
}
];
var standardSet = [
// There are questions that we need to ask in the CLI
// There are questions that we need to aks 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; }
@ -264,9 +235,10 @@ 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
@ -333,189 +305,92 @@ function askForConfig(state, mainCb) {
next();
}
var RC;
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 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(' '));
function finish() {
if (200 !== resp.statusCode) {
console.warn(resp.statusCode);
console.warn(body || ('get' + service + ' failed'));
//cb(new Error("not okay"), body);
return;
}
if (err) {
if (!body) { fn(null, null); return; }
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
fn(null, 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) {
// 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) {
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) {
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(err);
return;
} else {
}
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, 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;
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) {
if (!state.config.relay || !state.config.token) {
if (!state.config.relay) {
state.config.relay = 'telebit.cloud';
}
//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) {
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);
});
} else {
parseCli(state);
}
});
return;
}
//console.log("no questioning:");
parseCli(state);
}
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;
}
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;
}
RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0]));
return true;
}
help();
return true;
})) {
return;
}
// Two styles:
// http 3000
// http modulename
function makeRpc(key) {
if (key !== argv[0]) {
return false;
}
RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0]));
return true;
}
if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) {
return;
}
help();
process.exit(11);
}
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 || {}) || {};
RC = require('../lib/rc/index.js').create(state);
if (!Object.keys(state._clientConfig).length) {
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
console.info("");
}
if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) {
if (!err || 'ENOENT' === err.code) {
//console.warn("Empty config file. Run 'telebit init' to configure.\n");
} else {
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
}
}
function handleRemoteRequest(service, fn) {
return function (err, body) {
function finish() {
if ('function' === typeof fn) {
fn(err, body); // XXX was resp
fn(null, resp);
return;
}
console.info("");
if (err) {
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(err.statusCode, err.message);
console.warn(resp.statusCode, body);
//cb(new Error("not okay"), body);
return;
}
@ -529,7 +404,6 @@ function parseConfig(err, text) {
body = JSON.parse(body);
} catch(e) {
// ignore
}
if ("AWAIT_AUTH" === body.code) {
@ -541,43 +415,77 @@ function parseConfig(err, text) {
} 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) {
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;
}
state.relay = state.config.relay;
// { _otp, config: {} }
@ -603,7 +511,17 @@ function parseConfig(err, text) {
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));
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("");
}
next();
@ -614,16 +532,16 @@ function parseConfig(err, text) {
state._connecting = true;
// TODO use php-style object querification
RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) {
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;
}
console.info("waiting...");
console.info(TPLS.remote.waiting.replace(/{email}/, state.config.email));
next();
}));
});
}
, offer: function (token, next) {
//console.log("[offer] Pairing Enabled by Relay");
@ -633,12 +551,12 @@ function parseConfig(err, text) {
}
state._connecting = true;
try {
JWT.decode(token);
//console.log(JWT.decode(token));
require('jsonwebtoken').decode(token);
//console.log(require('jsonwebtoken').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*/) {
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) {
if (err) {
state._error = err;
console.error("Error while initializing config [offer]:");
@ -647,87 +565,169 @@ function parseConfig(err, text) {
}
//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) {
utils.putConfig('enable', [], function (err) {
if (err) { console.error(err); return; }
console.info("Success");
console.info(TPLS.remote.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();
console.info(TPLS.remote.next_steps);
process.exit(0);
}, 0.5 * 1000);
return;
}
// end workaround
//parseCli(state);
fn();
}));
parseCli(state);
});
}
});
}
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);
}));
function parseCli(/*state*/) {
var special = [
'false', 'none', 'off', 'disable'
, 'true', 'auto', 'on', 'enable'
];
if (-1 !== argv.indexOf('init')) {
utils.putConfig('list', []/*, function (err) {
}*/);
return;
}
bootstrap();
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;
}
utils.putConfig(argv[0], argv.slice(1));
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';
}
//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;
}
//console.log("no questioning:");
parseCli(state);
}
function parseConfig(err, text) {
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(
// 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);
if (!Object.keys(state._clientConfig).length) {
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
console.info("");
}
if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) {
if (!err || 'ENOENT' === err.code) {
//console.warn("Empty config file. Run 'telebit init' to configure.\n");
} else {
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
}
}
utils.request({ service: 'config' }, handleConfig);
}
var parsers = {
@ -807,41 +807,6 @@ var parsers = {
}
};
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);
});
});
});
});
}());

View File

@ -21,7 +21,6 @@ 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');

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
<!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>

View File

@ -1,234 +0,0 @@
<!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>

View File

@ -1,513 +0,0 @@
;(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);
}());

View File

@ -1,116 +0,0 @@
;(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));

View File

@ -1,338 +0,0 @@
;(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));

File diff suppressed because it is too large Load Diff

View File

@ -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('./jwt.js');
var jwt = require('jsonwebtoken');
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) {

View File

@ -1,102 +0,0 @@
'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;
};

View File

@ -6,7 +6,7 @@ Telebit Remote is the T-Rex long-arm of the Internet. UNSTOPPABLE!
Using reliable HTTPS tunneling to establishing peer-to-peer connections,
Telebit is empowering the next generation of tinkerers. Access your devices.
Share your stuff. Be UNSTOPPABLE! (Join us at https://ppl.family)
Share your stuff. Be UNSTOPPABLE! (Join us at https://rootprojects.org)
Usage:
@ -135,7 +135,7 @@ usage: telebit http <path/port/none> [subdomain]
Use cases:
- Lazy man's AirDrop (works or lazy women too!)
- Lazy man's AirDrop (works for lazy women too!)
- Testing dev sites on a phone
- Sharing indie music and movies with friends"
@ -145,7 +145,7 @@ usage: telebit ssh <auto|port|none>
All https traffic will be inspected to see if it looks like ssh Once enabled all traffic that looks
ssh auto Make ssh Just Works (on port 22)
ssh auto Make ssh Just Work (on port 22)
ssh <port> forward ssh traffic to non-standard port
ex: telebit ssh 22 ex: explicitly forward ssh-looking packets to localhost:22
@ -464,6 +464,19 @@ code = "
==============================================
"
waiting = "waiting for you to check your email..."
success = "Success"
next_steps = "Some fun things to try first:
~/telebit http ~/Public
~/telebit tcp 5050
~/telebit ssh auto
Press any key to continue...
"
[remote.setup]
email = "Welcome!
@ -476,13 +489,5 @@ 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}"

View File

@ -51,18 +51,24 @@
<div>
<h2>You've claimed <span class="js-servername">{{servername}}</span></h2>
<p>Here's some ways you can use it:</p>
<p>Here are some ways you can use Telebit via Terminal or other Command Line Interface:</p>
<div class="code-block">
<pre><code>telebit http ~/Public # serve a public folder
telebit http 3000 # forward all https traffic to localhost:3000
telebit http none # remove all https handlers</code></pre>
<br />
<pre><code>~/telebit ssh auto # allows you to connect to your computer with <br /> ssh-over-https from a different computer</span></code></pre>
<pre><code>~/telebit http ~/Public # serve a public folder
~/telebit http 3000 # forward all https traffic to localhost:3000
~/telebit http none # remove all https handlers</code></pre>
</div>
</div>
<p>You can <em>always</em> tunnel <strong>SSH over HTTPS</strong>,
<p>And remember you can <em>always</em> tunnel <strong>SSH over HTTPS</strong>,
even while you're using it for something else:</p>
<div class="code-block"><pre><code>telebit ssh auto</code></pre>
<p>&nbsp;</p>
<details>
<p><summary><strong>Here are some examples for those of you that want to access files and folders remotely. </strong></summary></p>
<p><strong>This function allows you to connect one computer to another computer you also have SSH on.</strong></p>
<div class="code-block"><pre><code>~/telebit ssh <span class="js-servername">{{servername}}</span></code></pre>
<br>
<pre><code>telebit ssh <span class="js-servername">{{servername}}</span></code></pre>
- or -
<pre><code>ssh -o ProxyCommand='<a href="https://telebit.cloud/sclient">sclient</a> %h' <span class="js-servername">{{servername}}</span></code></pre>
- or -
@ -70,8 +76,7 @@ telebit http none # remove all https handlers</code></pre>
ssh -o ProxyCommand="$proxy_cmd" <span class="js-servername">{{servername}}</span></code></pre>
</div>
<pre><code>ssh -o ProxyCommand='openssl s_client -connect %h:443 -servername %h -quiet' <span class="js-servername">{{servername}}</span></code></pre>
</details>
<!--div class="js-port" hidden>
<h2>You've claimed port <span class="js-serviceport">{{serviceport}}</span></h2>
<p>Here's some ways you can use it:</p>

View File

@ -3,7 +3,7 @@
document.body.hidden = false;
var hash = window.location.hash.substr(1);
var hash = window.location.hash.replace(/^[\/#?]+/, '');
var query = window.location.search;
function parseQuery(search) {

View File

@ -1,19 +0,0 @@
'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');

View File

@ -1,43 +0,0 @@
'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;
};

View File

@ -1,37 +0,0 @@
'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
};
};

View File

@ -1,22 +0,0 @@
'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;
});

View File

@ -1,49 +0,0 @@
'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;
}

View File

@ -1,175 +0,0 @@
'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;
};

View File

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

View File

@ -1,76 +0,0 @@
'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 generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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 && /\btelebit(d| daemon)\b/i.test(proc.cmd)) {
if ('node' === proc.name && /\btelebitd\b/i.test(proc.cmd)) {
console.log(proc);
process.kill(proc.pid);
return true;
@ -45,7 +45,37 @@ Launcher._detect = function (things, fn) {
}
}
require('./which.js').launcher(things._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 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');
});
}
};
Launcher.install = function (things, fn) {
if (!fn) { fn = function (err) { if (err) { console.error(err); } }; }

View File

@ -133,8 +133,11 @@ my_tmp="$(mktemp -d -t telebit.XXXXXXXX)"
#TELEBIT_TMP="$my_tmp/telebit"
echo "Installing $my_name to '$TELEBIT_REAL_PATH'"
# v10.2+ has much needed networking fixes, but breaks ursa. v9.x has severe networking bugs. v8.x has working ursa, but requires tls workarounds"
NODEJS_VER="${NODEJS_VER:-v10.6}"
# v10.2+ has much needed networking fixes, but breaks ursa.
# v9.x has severe networking bugs.
# v8.x has working ursa, but requires tls workarounds"
# v10.13 seems to work for me locally (new greenlock)
NODEJS_VER="${NODEJS_VER:-v10.13}"
export NODEJS_VER
export NODE_PATH="$TELEBIT_TMP/lib/node_modules"
export NPM_CONFIG_PREFIX="$TELEBIT_TMP"
@ -493,7 +496,8 @@ elif [ "systemd" == "$my_system_launcher" ]; then
else
echo -n "."
fi
systemctl --user daemon-reload
set +e
if systemctl --user daemon-reload; then
# enable also puts success output to stderr... why?
systemctl --user enable $my_app >/dev/null 2>/dev/null
#echo " > systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer"
@ -503,13 +507,19 @@ elif [ "systemd" == "$my_system_launcher" ]; then
fi
systemctl --user stop $my_app >/dev/null 2>/dev/null
systemctl --user start $my_app >/dev/null
sleep 2; # give it time to start
_is_running=$(systemctl --user status --no-pager $my_app 2>/dev/null | grep "active.*running")
if [ -z "$_is_running" ]; then
echo "Something went wrong:"
systemctl --user status --no-pager $my_app
exit 1
fi
else
echo "libpam-systemd is missing, which is required on Linux to register Telebit with the user launcher."
echo "sudo apt-get install -y libpam-systemd"
sudo apt-get install -y libpam-systemd
fi
set -e
echo -n "."
else

View File

@ -1,63 +0,0 @@
'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);
});
}
};