diff --git a/bin/telebit.js b/bin/telebit.js index 90f2e35..164df7e 100644 --- a/bin/telebit.js +++ b/bin/telebit.js @@ -33,10 +33,12 @@ function help() { console.info(''); console.info('Usage:'); console.info(''); - console.info('\ttelebit [--config ] '); + 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'); @@ -82,8 +84,202 @@ if (!confpath || /^--/.test(confpath)) { process.exit(1); } -function askForConfig() { - console.log("Please create a config file at '" + confpath + "' or specify --config /path/to/config"); +function askForConfig(answers, mainCb) { + answers = answers || {}; + //console.log("Please create a config file at '" + confpath + "' or specify --config /path/to/config"); + var readline = require('readline'); + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + // 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) { + if (!email) { askEmail(cb); return; } + answers.email = email.trim(); + answers.agree_tos = true; + console.info(""); + setTimeout(cb, 250); + }); + } + , 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 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'; + } + answers.relay = relay.trim(); + setTimeout(cb, 250); + }); + } + , function checkRelay(cb) { + if (!/\btelebit\.cloud\b/i.test(answers.relay)) { + standardSet = standardSet.concat(advancedSet); + } + nextSet = standardSet; + cb(); + } + ]; + var standardSet = [ + 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.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) { + // delete answers.token; + } + 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.join(','); + 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.join(','); + setTimeout(cb, 250); + }); + } + ]; + var nextSet = firstSet; + + function next() { + var q = nextSet.shift(); + if (!q) { rl.close(); mainCb(null, answers); return; } + q(next); + } + + next(); } function parseConfig(err, text) { @@ -94,7 +290,7 @@ function parseConfig(err, text) { if ('ENOENT' === err.code) { text = 'relay: \'\''; } - askForConfig(); + //askForConfig(); } try { @@ -125,12 +321,15 @@ function parseConfig(err, text) { 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); } else { if (body) { console.info('Response'); console.info(body); + //cb(null, body); } else { console.info("👌"); + //cb(null, ""); } } } @@ -179,6 +378,47 @@ function parseConfig(err, text) { 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) { + // TODO use php-style object querification + putConfig('config', Object.keys(answers).map(function (key) { + return key + ':' + answers[key]; + })); + /* TODO + if [ "telebit.cloud" == $my_relay ]; then + echo "" + echo "" + echo "==============================================" + echo " Hey, Listen! " + echo "==============================================" + echo "" + echo "GO CHECK YOUR EMAIL" + echo "" + echo "You MUST verify your email address to activate this device." + echo "(if the activation link expires, just run 'telebit restart' and check your email again)" + echo "" + $read_cmd -p "hit [enter] once you've clicked the verification" my_ignore + fi + */ + }); + return; + } + if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) { return; } diff --git a/bin/telebitd.js b/bin/telebitd.js index 1a38ec3..4e31983 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -47,6 +47,10 @@ function help() { var verstr = '' + pkg.name + ' v' + pkg.version; if (-1 === confIndex) { + // We have two possible valid paths if no --config is given (i.e. run from an npm-only install) + // * {install}/etc/telebitd.yml + // * ~/.config/telebit/telebitd.yml + // We'll asume the later since the installers include --config in the system launcher script confpath = path.join(state.homedir, '.config/telebit/telebitd.yml'); verstr += ' (--config "' + confpath + '")'; } @@ -87,11 +91,18 @@ function serveControls() { } function listSuccess() { - res.end(YAML.safeDump({ + var dumpy = { servernames: state.servernames , ports: state.ports , ssh: state.config.sshAuto || 'disabled' - })); + }; + + if (/\btelebit\.cloud\b/i.test(conf.relay) && state.config.email && !state.token) { + dumpy.code = "AWAIT_AUTH"; + dumpy.message = "Check your email. You must verify your email address to activate this device."; + } + + res.end(YAML.safeDump(dumpy)); } function sshSuccess() { @@ -105,6 +116,91 @@ function serveControls() { }); } + if (/\binit\b/.test(opts.path)) { + var conf = {}; + var fresh; + if (!opts.body) { + res.statusCode = 422; + res.end('{"error":{"message":"needs more arguments"}}'); + return; + } + // relay, email, agree_tos, servernames, ports + // + opts.body.forEach(function (opt) { + var parts = opt.split(/,/); + conf[parts[0]] = parts[1]; + }); + if (!state.config.relay || !state.config.email || !state.config.agreeTos) { + fresh = true; + } + + // TODO camelCase query + state.config.email = conf.email || state.config.email || ''; + if ('undefined' !== typeof conf.agreeTos) { + state.config.agreeTos = conf.agree_tos; + } + state.config.relay = conf.relay || state.config.relay || ''; + state.config.token = conf.token || state.config.token || null; + state.config.secret = conf.secret || state.config.secret || null; + if ('undefined' !== typeof conf.newsletter) { + state.config.newsletter = conf.newsletter; + } + if ('undefined' !== typeof conf.community_member) { + state.config.communityMember = conf.community_member; + } + if ('undefined' !== typeof conf.telemetry) { + state.config.telemetry = conf.telemetry; + } + conf.servernames.forEach(function (key) { + if (!state.config.servernames[key]) { + state.config.servernames[key] = {}; + } + }); + conf.ports.forEach(function (key) { + if (!state.config.ports[key]) { + state.config.ports[key] = {}; + } + }); + + if (!state.config.relay || !state.config.email || !state.config.agreeTos) { + res.statusCode = 400; + res.end('{"error":{"code":"E_CONFIG","message":"Missing important config file params. Please run \'telebit init\'"}}'); + return; + } + + if (tun) { + tun.end(function () { + tun = rawTunnel(); + }); + tun = null; + setTimeout(function () { + if (!tun) { tun = rawTunnel(); } + }, 3000); + } else { + tun = rawTunnel(); + } + + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + if (err) { + res.statusCode = 500; + res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'") + + '.\nPerhaps check that the file exists and your user has permissions to write it?"}}'); + return; + } + + // TODO check for message from remote about email + if (/\btelebit\.cloud\b/i.test(conf.relay) && state.config.email && !state.token) { + res.statusCode = 200; + res.end('{"success":true,"code":"AWAIT_AUTH","message":"Check your email. You must verify your email address to activate this device."}'); + } else { + res.statusCode = 200; + res.end('{"success":true}'); + } + }); + + return; + } + if (!state.config.relay || !state.config.email || !state.config.agreeTos) { res.statusCode = 400; res.end('{"error":{"code":"E_CONFIG","message":"Invalid config file. Please run \'telebit init\'"}}'); diff --git a/etc/.gitkeep b/etc/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/cli-common.js b/lib/cli-common.js index 05a6706..7fa6e76 100644 --- a/lib/cli-common.js +++ b/lib/cli-common.js @@ -7,7 +7,7 @@ var mkdirp = require('mkdirp'); var os = require('os'); var homedir = os.homedir(); -var localshare = '.local/share/telebit/var'; +var localshare = '.local/share/telebit'; var localconf = '.config/telebit'; common.pipename = function (config) { @@ -17,10 +17,20 @@ common.pipename = function (config) { } return pipename; }; -common.DEFAULT_SOCK_NAME = path.join(homedir, localshare, 'telebit.sock'); +common.DEFAULT_SOCK_NAME = path.join(homedir, localshare, 'var', 'telebit.sock'); try { - mkdirp.sync(path.join(homedir, localshare)); + mkdirp.sync(path.join(__dirname, '..', 'var', 'log')); + mkdirp.sync(path.join(__dirname, '..', 'var', 'run')); + mkdirp.sync(path.join(__dirname, '..', 'etc')); +} catch(e) { + console.error(e); +} + +try { + mkdirp.sync(path.join(homedir, localshare, 'var', 'log')); + mkdirp.sync(path.join(homedir, localshare, 'var', 'run')); + //mkdirp.sync(path.join(homedir, localshare, 'etc')); mkdirp.sync(path.join(homedir, localconf)); } catch(e) { console.error(e); diff --git a/lib/remote.js b/lib/remote.js index 6967724..75e4908 100644 --- a/lib/remote.js +++ b/lib/remote.js @@ -520,7 +520,7 @@ function _connect(state) { var connPromise; return { - end: function() { + end: function(cb) { tokens.length = 0; if (timeoutId) { clearTimeout(timeoutId); @@ -529,7 +529,7 @@ function _connect(state) { if (wstunneler) { try { - wstunneler.close(); + wstunneler.close(cb); } catch(e) { console.error("[error] wstunneler.close()"); console.error(e); diff --git a/usr/share/install_helper.sh b/usr/share/install_helper.sh index e3fcbfb..8a4fba1 100644 --- a/usr/share/install_helper.sh +++ b/usr/share/install_helper.sh @@ -285,134 +285,28 @@ else fi sleep 2 -echo "" -echo "" -echo "===============================================" -echo " Service Configuration " -echo "===============================================" -echo "" - -if [ -z "${my_email}" ]; then - echo "" - echo "" - echo "Telebit uses Greenlock for free automated ssl through Let's Encrypt." - echo "" - echo "To accept the Terms of Service for Telebit, Greenlock and Let's Encrypt," - echo "please enter your email." - echo "" - $read_cmd -p "email: " my_email - echo "" - # UX - just want a smooth transition - sleep 0.25 -fi - -if [ -z "${my_relay}" ]; then - echo "What relay will you be using? (press enter for default)" - echo "" - $read_cmd -p "relay [default: telebit.cloud]: " my_relay - echo "" - my_relay=${my_relay:-telebit.cloud} - # UX - just want a smooth transition - sleep 0.25 -fi - -if [ -n "$my_relay" ] && [ "$my_relay" != "telebit.cloud" ]; then - if [ -z "${my_servernames}" ]; then - #echo "What servername(s) will you be relaying here? (press enter for default)" - echo "What servername(s) will you be relaying here?" - echo "" - #$read_cmd -p "domain [default: .telebit.cloud]: " my_servernames - $read_cmd -p "domain: " my_servernames - echo "" - # UX - just want a smooth transition - sleep 0.25 - fi - - if [ -z "${my_secret}" ]; then - #echo "What's your authorization for the relay server? (press enter for default)" - echo "What's your authorization for the relay server?" - echo "" - #$read_cmd -p "auth [default: new account]: " my_secret - $read_cmd -p "secret: " my_secret - echo "" - # UX - just want a smooth transition - sleep 0.25 - fi -fi # TODO don't create this in TMP_PATH if it exists in TELEBIT_PATH my_config="$TELEBIT_PATH/etc/$my_daemon.yml" mkdir -p "$(dirname $my_config)" if [ ! -e "$my_config" ]; then - #$rsync_cmd examples/$my_app.yml "$my_config" - - if [ -n "$my_email" ]; then - echo "email: $my_email" >> "$my_config" - echo "agree_tos: true" >> "$my_config" - else - echo "#email: jon@example.com # used for Automated HTTPS and Telebit.Cloud registrations" >> "$my_config" - echo "#agree_tos: true # must be enabled to use Automated HTTPS and Telebit.Cloud" >> "$my_config" - fi - echo "sock: $TELEBIT_PATH/var/telebit.sock" >> "$my_config" - - if [ -n "$my_relay" ]; then - echo "relay: $my_relay" >> "$my_config" - - if [ -n "$my_secret" ]; then - echo "secret: $my_secret" >> "$my_config" - fi - if [ -n "$my_servernames" ]; then - # TODO could use printf or echo -e, - # just not sure how portable they are - echo "servernames:" >> "$my_config" - echo " $my_servernames: {}" >> "$my_config" - fi - else - echo "relay: telebit.cloud # the relay server to use" >> "$my_config" - fi - #echo "dynamic_ports:\n []" >> "$my_config" + echo "sock: $TELEBIT_PATH/var/run/telebit.sock" >> "$my_config" cat $TELEBIT_PATH/usr/share/$my_daemon.tpl.yml >> "$my_config" fi -#my_config_link="/etc/$my_app/$my_app.yml" -#if [ ! -e "$my_config_link" ]; then -# echo "${sudo_cmde}ln -sf '$my_config' '$my_config_link'" -# #$sudo_cmd mkdir -p /etc/$my_app -# $sudo_cmd ln -sf "$my_config" "$my_config_link" -#fi - my_config="$HOME/.config/$my_app/$my_app.yml" mkdir -p "$(dirname $my_config)" if [ ! -e "$my_config" ]; then - echo "cli: true" >> "$my_config" - echo "sock: $TELEBIT_PATH/var/telebit.sock" >> "$my_config" + echo "sock: $TELEBIT_PATH/var/run/telebit.sock" >> "$my_config" - if [ -n "$my_email" ]; then - echo "email: $my_email" >> "$my_config" - echo "agree_tos: true" >> "$my_config" - else - echo "#email: jon@example.com # used for Automated HTTPS and Telebit.Cloud registrations" >> "$my_config" - echo "#agree_tos: true # must be enabled to use Automated HTTPS and Telebit.Cloud" >> "$my_config" - fi - - if [ -n "$my_relay" ]; then - echo "relay: $my_relay" >> "$my_config" - - if [ -n "$my_secret" ]; then - echo "secret: $my_secret" >> "$my_config" - fi - else - echo "relay: telebit.cloud # the relay server to use" >> "$my_config" - fi fi #echo "${sudo_cmde}chown -R $my_user '$TELEBIT_PATH' $sudo_cmd chown -R $my_user "$TELEBIT_PATH" - ############################### # Actually Launch the Service # ############################### @@ -425,45 +319,8 @@ if [ "systemd" == "$my_system_launcher" ]; then $sudo_cmd systemctl restart $my_app fi -# TODO run 'telebit status' +echo "telebit init" sleep 2 -if [ "telebit.cloud" == $my_relay ]; then - echo "" - echo "" - echo "==============================================" - echo " Hey, Listen! " - echo "==============================================" - echo "" - echo "GO CHECK YOUR EMAIL" - echo "" - echo "You MUST verify your email address to activate this device." - echo "(if the activation link expires, just run 'telebit restart' and check your email again)" - echo "" - $read_cmd -p "hit [enter] once you've clicked the verification" my_ignore -fi -sleep 2 -echo "" -echo "" -echo "" -echo "==============================================" -echo " Privacy Settings " -echo "==============================================" -echo "" -echo "Privacy settings are managed in the config files:" -echo "" -echo " $TELEBIT_PATH/etc/$my_app.yml" -echo " $HOME/.config/$my_app/$my_app.yml" -echo "" -echo "Your current settings:" -echo "" -echo " telemetry: true # You ARE contributing project telemetry" -echo " community: true # You ARE receiving important email updates" -echo " newsletter: false # You ARE NOT receiving regular emails" -echo "" -echo "Please edit the config file to meet your needs before starting." -echo "" -sleep 1 - -echo "" -sleep 1 +$TELEBIT_PATH/bin/node $TELEBIT_PATH/bin/telebit.js init +$TELEBIT_PATH/bin/node $TELEBIT_PATH/bin/telebit.js enable diff --git a/usr/share/telebitd.tpl.yml b/usr/share/telebitd.tpl.yml index 4846463..ca08fd5 100644 --- a/usr/share/telebitd.tpl.yml +++ b/usr/share/telebitd.tpl.yml @@ -1,4 +1,5 @@ #agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes community_member: true # receive infrequent relevant updates telemetry: true # contribute to project telemetric data -ssh_auto: 22 # forward ssh-looking packets, from any connection, to port 22 +newsletter: false # contribute to project telemetric data +ssh_auto: false # forward ssh-looking packets, from any connection, to port 22 diff --git a/var/log/.gitkeep b/var/log/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/var/run/.gitkeep b/var/run/.gitkeep new file mode 100644 index 0000000..e69de29