#!/usr/bin/env node (function () { 'use strict'; var pkg = require('../package.json'); var os = require('os'); //var url = require('url'); var path = require('path'); var http = require('http'); var https = require('https'); var YAML = require('js-yaml'); var recase = require('recase').create({}); var camelCopy = recase.camelCopy.bind(recase); //var snakeCopy = recase.snakeCopy.bind(recase); var urequest = require('@coolaj86/urequest'); var common = require('../lib/cli-common.js'); var argv = process.argv.slice(2); var argIndex = argv.indexOf('--config'); var confpath; var useTty; var state = {}; if (-1 === argIndex) { argIndex = argv.indexOf('-c'); } if (-1 !== argIndex) { confpath = argv.splice(argIndex, 2)[1]; } argIndex = argv.indexOf('--tty'); if (-1 !== argIndex) { useTty = argv.splice(argIndex, 1); } function help() { console.info(''); console.info('Telebit Remote v' + pkg.version); console.info(''); console.info('Usage:'); console.info(''); console.info('\ttelebit [--config ] '); console.info(''); console.info('Examples:'); console.info(''); //console.info('\ttelebit init # bootstrap the config files'); //console.info(''); console.info('\ttelebit status # whether enabled or disabled'); console.info('\ttelebit enable # disallow incoming connections'); console.info('\ttelebit disable # allow incoming connections'); console.info(''); console.info('\ttelebit list # list rules for servernames and ports'); console.info(''); console.info('\ttelebit http none # remove all https handlers'); console.info('\ttelebit http 3000 # forward all https traffic to port 3000'); console.info('\ttelebit http /module/path # load a node module to handle all https traffic'); console.info(''); console.info('\ttelebit http none example.com # remove https handler from example.com'); console.info('\ttelebit http 3001 example.com # forward https traffic for example.com to port 3001'); console.info('\ttelebit http /module/path example.com # forward https traffic for example.com to port 3001'); console.info(''); console.info('\ttelebit tcp none # remove all tcp handlers'); console.info('\ttelebit tcp 5050 # forward all tcp to port 5050'); console.info('\ttelebit tcp /module/path # handle all tcp with a node module'); console.info(''); console.info('\ttelebit tcp none 6565 # remove tcp handler from external port 6565'); console.info('\ttelebit tcp 5050 6565 # forward external port 6565 to local 5050'); console.info('\ttelebit tcp /module/path 6565 # handle external port 6565 with a node module'); console.info(''); console.info('Config:'); console.info(''); console.info('\tSee https://git.coolaj86.com/coolaj86/telebit.js'); console.info(''); console.info(''); } var verstr = [ pkg.name + ' remote v' + pkg.version ]; if (!confpath) { confpath = path.join(os.homedir(), '.config/telebit/telebit.yml'); verstr.push('(--config "' + confpath + '")'); } if (-1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) { help(); process.exit(0); } if (!confpath || /^--/.test(confpath)) { help(); process.exit(1); } function askForConfig(answers, mainCb) { answers = answers || {}; //console.log("Please create a config file at '" + confpath + "' or specify --config /path/to/config"); var fs = require('fs'); var stdin = useTty ? fs.createReadStream('/dev/tty') : process.stdin; var readline = require('readline'); var rl = readline.createInterface({ input: stdin , output: process.stdout // https://github.com/nodejs/node/issues/21319 , terminal: !useTty }); // NOTE: Use of setTimeout // We're using setTimeout just to make the user experience a little // nicer, as if we're doing something inbetween steps, so that it // is a smooth rather than jerky experience. // >= 300ms is long enough to become distracted and change focus (a full blink, time for an idea to form as a thought) // <= 100ms is shorter than normal human reaction time (ability to place events chronologically, which happened first) // ~ 150-250ms is the sweet spot for most humans (long enough to notice change and not be jarred, but stay on task) var firstSet = [ function askEmail(cb) { if (answers.email) { cb(); return; } console.info(""); console.info(""); console.info("Telebit uses Greenlock for free automated ssl through Let's Encrypt."); console.info(""); console.info("To accept the Terms of Service for Telebit, Greenlock and Let's Encrypt,"); console.info("please enter your email."); console.info(""); // TODO attempt to read email from npmrc or the like? rl.question('email: ', function (email) { email = /@/.test(email) && email.trim(); if (!email) { askEmail(cb); return; } answers.email = email.trim(); answers.agree_tos = true; console.info(""); setTimeout(cb, 250); }); } , function askRelay(cb) { if (answers.relay) { cb(); return; } console.info(""); console.info(""); console.info("What relay will you be using? (press enter for default)"); console.info(""); rl.question('relay [default: telebit.cloud]: ', function (relay) { // TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json if (!relay) { relay = 'telebit.cloud'; } relay = relay.trim(); var urlstr = common.parseUrl(relay) + common.apiDirectory; urequest({ url: urlstr, json: true }, function (err, resp, body) { if (err) { console.error("[Network Error] Failed to retrieve '" + urlstr + "'"); console.error(err); askRelay(cb); 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(body); } else if (body && body.pair_request) { answers._can_pair = true; } answers.relay = relay; cb(); }); }); } , function askAgree(cb) { if (answers.agree_tos) { cb(); return; } console.info(""); console.info(""); console.info("Do you accept the terms of service for each and all of the following?"); console.info(""); console.info("\tTelebit - End-to-End Encrypted Relay"); console.info("\tGreenlock - Automated HTTPS"); console.info("\tLet's Encrypt - TLS Certificates"); console.info(""); console.info("Type 'y' or 'yes' to accept these Terms of Service."); console.info(""); rl.question('agree to all? [y/N]: ', function (resp) { resp = resp.trim(); if (!/^y(es)?$/i.test(resp) && 'true' !== resp) { throw new Error("You didn't accept the Terms of Service... not sure what to do..."); } answers.agree_tos = true; console.info(""); setTimeout(cb, 250); }); } , function checkRelay(cb) { if (!answers._can_pair) { standardSet = standardSet.concat(advancedSet); } nextSet = standardSet; cb(); } ]; var standardSet = [ function askUpdates(cb) { var options = [ 'newsletter', 'important', 'required' ]; if (-1 !== options.indexOf(answers.updates)) { cb(); return; } console.info(""); console.info(""); console.info("What updates would you like to receive? (" + options.join(',') + ")"); console.info(""); rl.question('messages (default: important): ', function (updates) { updates = (updates || '').trim().toLowerCase(); if (!updates) { updates = 'important'; } if (-1 === options.indexOf(updates)) { askUpdates(cb); return; } if ('newsletter' === updates) { answers.newsletter = true; answers.communityMember = true; } else if ('important' === updates) { answers.communityMember = true; } setTimeout(cb, 250); }); } /* , function askNewsletter(cb) { if (answers.newsletter) { cb(); return; } console.info(""); console.info(""); console.info("Would you like to subscribe to our newsletter? (press enter for default [no])"); console.info(""); rl.question('newsletter [y/N] (default: no): ', function (newsletter) { if (/^y(es)?$/.test(newsletter)) { answers.newsletter = true; } setTimeout(cb, 250); }); } , function askCommunity(cb) { if (answers.community_member) { cb(); return; } console.info(""); console.info(""); console.info("Receive important and relevant updates? (press enter for default [yes])"); console.info(""); rl.question('community_member [Y/n]: ', function (community) { if (!community || /^y(es)?$/i.test(community)) { answers.community_member = true; } setTimeout(cb, 250); }); } */ , function askTelemetry(cb) { if (answers.telemetry) { cb(); return; } console.info(""); console.info(""); console.info("Contribute project telemetry data? (press enter for default [yes])"); console.info(""); rl.question('telemetry [Y/n]: ', function (telemetry) { if (!telemetry || /^y(es)?$/i.test(telemetry)) { answers.telemetry = true; } setTimeout(cb, 250); }); } ]; var advancedSet = [ function askTokenOrSecret(cb) { if (answers._can_pair || answers.token || answers.secret) { cb(); return; } console.info(""); console.info(""); console.info("What's your authorization for '" + answers.relay + "'?"); console.info(""); // TODO check .well-known to learn supported token types console.info("Currently supported:"); console.info(""); console.info("\tToken (JWT format)"); console.info("\tShared Secret (HMAC hex)"); //console.info("\tPrivate key (hex)"); console.info(""); rl.question('auth: ', function (resp) { var jwt = require('jsonwebtoken'); resp = (resp || '').trim(); try { answers.token = jwt.decode(resp); } catch(e) { // is not jwt try { if (JSON.parse(resp).subject) { answers.token = resp; } } catch(e) { // is not authRequest either } } if (!answers.token) { resp = resp.toLowerCase(); if (resp === Buffer.from(resp, 'hex').toString('hex')) { answers.secret = resp; } } if (!answers.token && !answers.secret) { askTokenOrSecret(cb); return; } setTimeout(cb, 250); }); } , function askServernames(cb) { if (!answers.secret || answers.servernames) { cb(); return; } console.info(""); console.info(""); console.info("What servername(s) will you be relaying here?"); console.info("(use a comma-separated list such as example.com,example.net)"); console.info(""); rl.question('domain(s): ', function (resp) { resp = (resp || '').trim().split(/,/g); if (!resp.length) { askServernames(); return; } // TODO validate the domains answers.servernames = resp; setTimeout(cb, 250); }); } , function askPorts(cb) { if (!answers.secret || answers.ports) { cb(); return; } console.info(""); console.info(""); console.info("What tcp port(s) will you be relaying here?"); console.info("(use a comma-separated list such as 2222,5050)"); console.info(""); rl.question('port(s) [default:none]: ', function (resp) { resp = (resp || '').trim().split(/,/g); if (!resp.length) { askPorts(); return; } // TODO validate the domains answers.ports = resp; setTimeout(cb, 250); }); } ]; var nextSet = firstSet; function next() { var q = nextSet.shift(); if (!q) { // https://github.com/nodejs/node/issues/21319 rl.close(); if (useTty) { stdin.close(); } mainCb(null, answers); return; } q(next); } next(); } function parseConfig(err, text) { console.info(""); console.info(verstr.join(' ')); try { state.config = JSON.parse(text || '{}'); } catch(e1) { try { state.config = YAML.safeLoad(text || '{}'); } catch(e2) { console.error(e1.message); console.error(e2.message); process.exit(1); return; } } state.config = camelCopy(state.config || {}) || {}; state._ipc = common.pipename(state.config, true); if (!Object.keys(state.config).length) { console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); } console.info(""); if ((err && 'ENOENT' === err.code) || !Object.keys(state.config).length) { if (!err || 'ENOENT' === err.code) { //console.warn("Empty config file. Run 'telebit init' to configure.\n"); } else { console.warn("Couldn't load config:\n\n\t" + err.message + "\n"); } } function putConfig(service, args, fn) { // console.log('got it', service, args); var req = http.get({ socketPath: state._ipc.path , method: 'POST' , path: '/rpc/' + service + '?_body=' + 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(YAML.safeDump(body)); } else { console.info(JSON.stringify(body, null, 2)); } } 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; }); } // Two styles: // http 3000 // http modulename function makeRpc(key) { if (key !== argv[0]) { return false; } putConfig(argv[0], argv.slice(1)); return true; } if ([ 'ssh', 'http', 'tcp' ].some(function (key) { if (key !== argv[0]) { return false; } if (argv[1]) { putConfig(argv[0], argv.slice(1)); return true; } help(); return true; })) { return true; } if (-1 !== argv.indexOf('init')) { var answers = {}; if ('init' !== argv[0]) { throw new Error("init must be the first argument"); } argv.shift(); argv.forEach(function (arg) { var parts = arg.split(/:/g); if (2 !== parts.length) { throw new Error("bad option to init: '" + arg + "'"); } if (answers[parts[0]]) { throw new Error("duplicate key to init '" + parts[0] + "'"); } answers[parts[0]] = parts[1]; }); askForConfig(answers, function (err, answers) { if (!answers.token && answers._can_pair) { answers._otp = common.otp(); console.log(""); console.log("=============================================="); console.log(" Hey, Listen! "); console.log("=============================================="); console.log(" "); console.log(" GO CHECK YOUR EMAIL! "); console.log(" "); console.log(" DEVICE PAIR CODE: 0000 ".replace(/0000/g, answers._otp)); console.log(" "); console.log("=============================================="); console.log(""); } // TODO use php-style object querification putConfig('config', Object.keys(answers).map(function (key) { return key + ':' + answers[key]; }), function (err, body) { if (err) { console.error("Error while initializing config:"); console.error(err); } // need just a little time to let the grants occur setTimeout(function () { putConfig('list', []); }, 1 * 1000); }); }); return; } if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) { return; } help(); } require('fs').readFile(confpath, 'utf8', parseConfig); }());