Confronta commit
63 Commit
Autore | SHA1 | Data |
---|---|---|
AJ ONeal | 1726e137b8 | |
AJ ONeal | ffc95b4ddf | |
AJ ONeal | 7f49650c48 | |
AJ ONeal | a5c448902e | |
AJ ONeal | ae452367c0 | |
AJ ONeal | 7a9cc7cb77 | |
AJ ONeal | 16be216c99 | |
AJ ONeal | 53cc3ccaba | |
AJ ONeal | 58dab177da | |
AJ ONeal | 1fda5b15d0 | |
AJ ONeal | 1c9396216a | |
AJ ONeal | 21115b9543 | |
AJ ONeal | 693a524549 | |
AJ ONeal | 583ffabdff | |
AJ ONeal | 75f538fa16 | |
AJ ONeal | d5a622444e | |
AJ ONeal | 0996d78ecd | |
AJ ONeal | 7758e3b5ed | |
AJ ONeal | fff8f318c0 | |
AJ ONeal | 535c9732c6 | |
AJ ONeal | 34bcf79d98 | |
AJ ONeal | 142fb0942c | |
AJ ONeal | 7f18482566 | |
AJ ONeal | 40921b58ff | |
AJ ONeal | d743c51d86 | |
AJ ONeal | b18a6aa01c | |
AJ ONeal | 08c18b8c94 | |
AJ ONeal | 2679a20d1c | |
AJ ONeal | ce20b058e6 | |
AJ ONeal | c5e7811028 | |
AJ ONeal | 4b64490bdc | |
AJ ONeal | 075342920d | |
AJ ONeal | 61a5af2124 | |
AJ ONeal | e6b7ba575f | |
AJ ONeal | 9e1c9c00ca | |
AJ ONeal | b81f0ecede | |
AJ ONeal | 40d78b463f | |
AJ ONeal | f0222baff6 | |
AJ ONeal | f2e60dae5e | |
AJ ONeal | 07e9bd7ed9 | |
AJ ONeal | 9827267620 | |
AJ ONeal | 81ee4b27a5 | |
AJ ONeal | b75552a287 | |
AJ ONeal | 66758f4dbf | |
AJ ONeal | 311e82b9b6 | |
AJ ONeal | 79d01e2c31 | |
AJ ONeal | 0bdaacb8aa | |
AJ ONeal | 26939a62cf | |
AJ ONeal | a6e4bda317 | |
AJ ONeal | e0ea17a377 | |
AJ ONeal | be7a895dc7 | |
AJ ONeal | 8f7e865d49 | |
AJ ONeal | 20ed109aeb | |
AJ ONeal | ebd4f530f2 | |
AJ ONeal | 68dac05d47 | |
AJ ONeal | 6c9d13f155 | |
AJ ONeal | aad592c893 | |
AJ ONeal | 82f5545d72 | |
AJ ONeal | 3e0c977511 | |
AJ ONeal | c3e9bbaa5a | |
AJ ONeal | f3e5945afc | |
AJ ONeal | 1fc04f05b5 | |
AJ ONeal | e16e5a34e6 |
|
@ -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
|
||||
=======
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
console.error(err);
|
||||
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;
|
||||
}
|
||||
|
||||
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 (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.info();
|
||||
}
|
||||
}
|
||||
|
||||
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: {} }
|
||||
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("");
|
||||
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();
|
||||
}
|
||||
, 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;
|
||||
}
|
||||
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");
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
var RC;
|
||||
|
||||
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 (err) {
|
||||
if ('ENOENT' === err.code || 'ECONNREFUSED' === 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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}());
|
||||
|
|
|
@ -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');
|
||||
|
|
1181
bin/telebitd.js
1181
bin/telebitd.js
File diff soppresso perché troppo grande
Carica Diff
|
@ -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>
|
|
@ -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 & 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 & 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>
|
|
@ -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);
|
||||
|
||||
}());
|
|
@ -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));
|
|
@ -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));
|
File diff soppresso perché troppo grande
Carica Diff
|
@ -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) {
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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}"
|
||||
|
|
|
@ -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');
|
|
@ -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;
|
||||
};
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
}
|
File diff soppresso perché troppo grande
Carica Diff
12
package.json
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"
|
||||
},
|
||||
|
|
|
@ -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); } }; }
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
Caricamento…
Fai riferimento in un nuovo problema