library-ize the token procedure a little bit

This commit is contained in:
AJ ONeal 2018-06-21 11:01:16 +00:00
parent 99b891fd99
commit fb1fafeb85
3 changed files with 304 additions and 187 deletions

View File

@ -212,7 +212,7 @@ function askForConfig(answers, mainCb) {
console.info("");
console.info("What updates would you like to receive? (" + options.join(',') + ")");
console.info("");
rl.question('email preference (default: important): ', function (updates) {
rl.question('messages (default: important): ', function (updates) {
updates = (updates || '').trim().toLowerCase();
if (!updates) { updates = 'important'; }
if (-1 === options.indexOf(updates)) { askUpdates(cb); return; }
@ -386,7 +386,7 @@ function parseConfig(err, text) {
}
}
function putConfig(service, args) {
function putConfig(service, args, fn) {
// console.log('got it', service, args);
var req = http.get({
socketPath: state._ipc.path
@ -395,6 +395,11 @@ function parseConfig(err, text) {
}, function (resp) {
function finish() {
if ('function' === typeof fn) {
fn(null, resp);
return;
}
console.info("");
if (200 !== resp.statusCode) {
console.warn("'" + service + "' may have failed."
@ -438,7 +443,7 @@ function parseConfig(err, text) {
}
});
req.on('error', function (err) {
console.error('Error');
console.error('Put Config Error:');
console.error(err);
return;
});
@ -485,27 +490,28 @@ function parseConfig(err, text) {
}
answers[parts[0]] = parts[1];
});
askForConfig(answers, function (err, answers) {
answers._otp = common.otp();
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("==============================================");
// 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
*/
}), function (err, body) {
// need just a little time to let the grants occur
setTimeout(function () {
makeRpc('list');
}, 1 * 1000);
});
});
return;
}

View File

@ -98,6 +98,9 @@ function serveControlsHelper() {
, ssh: state.config.sshAuto || 'disabled'
, code: 'CONFIG'
};
if (state.otp) {
dumpy.device_pair_code = state.otp;
}
if (state._can_pair && state.config.email && !state.token) {
dumpy.code = "AWAIT_AUTH";
@ -158,6 +161,7 @@ function serveControlsHelper() {
if ('undefined' !== typeof conf.agree_tos) {
state.config.agreeTos = conf.agree_tos;
}
state.otp = conf._otp || common.otp();
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;
@ -483,31 +487,93 @@ function parseConfig(err, text) {
}
}
function connectTunnel() {
function sigHandler() {
console.info('Received kill signal. Attempting to exit cleanly...');
// We want to handle cleanup properly unless something is broken in our cleanup process
// that prevents us from exitting, in which case we want the user to be able to send
// the signal again and exit the way it normally would.
process.removeListener('SIGINT', sigHandler);
tun.end();
controlServer.close();
function rawTunnel(rawCb) {
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
rawCb(null, null);
return;
}
// reverse 2FA otp
process.on('SIGINT', sigHandler);
state.net = state.net || {
createConnection: function (info, cb) {
// data is the hello packet / first chunk
// info = { data, servername, port, host, remoteFamily, remoteAddress, remotePort }
var net = require('net');
// socket = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
var socket = net.createConnection({ port: info.port, host: info.host }, cb);
return socket;
state.relay = state.config.relay;
if (!state.relay) {
rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
return;
}
};
common.api.token(state, {
error: function (err/*, next*/) {
console.error("[Error] common.api.token:");
console.error(err);
rawCb(err);
}
, directory: function (dir, next) {
console.log('Telebit Relay Discovered:');
state._apiDirectory = dir;
console.log(dir);
console.log();
next();
}
, tunnelUrl: function (tunnelUrl, next) {
console.log('Telebit Relay Tunnel Socket:', tunnelUrl);
state.wss = tunnelUrl;
next();
}
, requested: function (authReq, next) {
console.log("Pairing Requested");
var pin = authReq.pin || authReq.otp || authReq.pairCode;
state.otp = state._otp = pin;
state.auth = state.authRequest = state._auth = authReq;
console.info();
console.info('====================================');
console.info('= HEY! LISTEN! =');
console.info('====================================');
console.info('= =');
console.info('= 1. CHECK YOUR EMAIL =');
console.info('= =');
console.info('= 2. DEVICE PAIRING CODE: 0000 ='.replace('0000', pin));
console.info('= =');
console.info('====================================');
console.info();
next();
}
, connect: function (pretoken, next) {
console.log("Enabling Pairing Locally...");
connectTunnel(pretoken, function (err, _tun) {
console.log("Pairing Enabled Locally");
tun = _tun;
next();
});
}
, offer: function (token, next) {
console.log("Pairing Enabled by Relay");
state.token = token;
state.config.token = token;
state.handlers.access_token({ jwt: token });
if (tun) {
tun.append(token);
} else {
connectTunnel(token, function (err, _tun) {
tun = _tun;
});
}
next();
}
, granted: function (token, next) {
console.log("Relay-Remote Pairing Complete");
next();
}
, end: function () {
rawCb(null, tun);
}
});
}
function connectTunnel(token, cb) {
if (tun) {
cb(null, tun);
return;
}
state.greenlockConf = state.config.greenlock || {};
state.sortingHat = state.config.sortingHat;
@ -515,7 +581,6 @@ function connectTunnel() {
// TODO Check undefined vs false for greenlock config
var remote = require('../');
console.log();
state.greenlockConfig = {
version: state.greenlockConf.version || 'draft-11'
, server: state.greenlockConf.server || 'https://acme-v02.api.letsencrypt.org/directory'
@ -546,7 +611,7 @@ function connectTunnel() {
state.insecure = state.config.relay_ignore_invalid_certificates;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
var tun = remote.connect({
tun = remote.connect({
relay: state.relay
, wss: state.wss
, config: state.config
@ -554,146 +619,14 @@ function connectTunnel() {
, sortingHat: state.sortingHat
, net: state.net
, insecure: state.insecure
, token: state.token
, token: token // instance
, servernames: state.servernames
, ports: state.ports
, handlers: state.handlers
, greenlockConfig: state.greenlockConfig
});
return tun;
}
function rawTunnel(cb) {
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
cb(null, null);
return;
}
state.relay = state.config.relay;
if (!state.relay) {
cb(new Error("'" + state._confpath + "' is missing 'relay'"));
return;
}
state.relayUrl = common.parseUrl(state.relay);
state.relayHostname = common.parseHostname(state.relay);
urequest({ url: state.relayUrl + common.apiDirectory, json: true }, function (err, resp, body) {
state._apiDirectory = body;
state.wss = body.tunnel.method + '://' + body.api_host.replace(/:hostname/g, state.relayHostname) + body.tunnel.pathname;
console.log('api dir:');
console.log(body);
console.log('state.wss:');
console.log(state.wss);
if (!state.config.token && state.config.secret) {
var jwt = require('jsonwebtoken');
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)
};
state.token = jwt.sign(tokenData, state.config.secret);
}
state.token = state.token || state.config.token;
if (state.token) { cb(null, connectTunnel()); return; }
if (!state.config.email) {
cb(new Error("No email... how did that happen?"));
return;
}
// TODO sign token with own private key, including public key and thumbprint
// (much like ACME JOSE account)
state.otp = common.otp();
state._auth = {
subject: state.config.email
, subject_scheme: 'mailto'
// TODO create domains list earlier
, scope: Object.keys(state.config.servernames || {}).join(',')
, otp: state.otp
, hostname: os.hostname()
// Used for User-Agent
, os_type: os.type()
, os_platform: os.platform()
, os_release: os.release()
, os_arch: os.arch()
};
if (state.config.email && !state.token) {
console.info();
console.info('====================================');
console.info('= HEY! LISTEN! =');
console.info('====================================');
console.info('= =');
console.info('= 1. Open your email =');
console.info('= =');
console.info('= 2. Click the magic login link =');
console.info('= Login Code (if needed): 0000 ='.replace('0000', state.otp));
console.info('= =');
console.info('= 3. Check back here for deets =');
console.info('= =');
console.info('= =');
console.info('====================================');
console.info();
}
if (err || !body || !body.pair_request) {
cb(null, connectTunnel());
return;
}
// TODO do auth stuff
var pairRequestUrl = url.resolve('https://' + body.api_host.replace(/:hostname/g, state.relayHostname), body.pair_request.pathname);
var req = {
url: pairRequestUrl
, method: body.pair_request.method
, json: state._auth
};
console.log('[telebitd.js] req');
console.log(req);
function gotoNext(req) {
urequest(req, function (err, resp, body) {
if (err) { console.error('[telebitd.js] pair request', err); return; }
console.log('\nToken Request Body:');
console.log(resp.headers);
console.log(body);
console.info('Device Pair Code: 0000'.replace('0000', state.otp));
// pending, try again
if (resp.headers.location) {
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
return;
}
if ('ready' !== body.status) {
console.error("\n[error] neither ready nor pending...");
console.error(body);
return;
}
state.token = body.access_token;
state.config.token = state.token;
state.handlers.access_token({ jwt: state.token });
cb(null, connectTunnel());
});
}
gotoNext(req);
});
cb(null, tun);
}
state.handlers = {
@ -746,6 +679,33 @@ state.handlers = {
}
};
function sigHandler() {
console.info('Received kill signal. Attempting to exit cleanly...');
// We want to handle cleanup properly unless something is broken in our cleanup process
// that prevents us from exitting, in which case we want the user to be able to send
// the signal again and exit the way it normally would.
process.removeListener('SIGINT', sigHandler);
if (tun) {
tun.end();
}
controlServer.close();
}
// reverse 2FA otp
process.on('SIGINT', sigHandler);
state.net = state.net || {
createConnection: function (info, cb) {
// data is the hello packet / first chunk
// info = { data, servername, port, host, remoteFamily, remoteAddress, remotePort }
var net = require('net');
// socket = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
var socket = net.createConnection({ port: info.port, host: info.host }, cb);
return socket;
}
};
require('fs').readFile(confpath, 'utf8', parseConfig);
}());

View File

@ -3,9 +3,11 @@
var common = module.exports;
var path = require('path');
var url = require('url');
var mkdirp = require('mkdirp');
var os = require('os');
var homedir = os.homedir();
var urequest = require('@coolaj86/urequest');
var localshare = '.local/share/telebit';
var localconf = '.config/telebit';
@ -27,7 +29,6 @@ common.pipename = function (config, newApi) {
common.DEFAULT_SOCK_NAME = path.join(homedir, localshare, 'var', 'run', 'telebit.sock');
common.parseUrl = function (hostname) {
var url = require('url');
var location = url.parse(hostname);
if (!location.protocol || /\./.test(location.protocol)) {
hostname = 'https://' + hostname;
@ -38,7 +39,6 @@ common.parseUrl = function (hostname) {
return hostname;
};
common.parseHostname = function (hostname) {
var url = require('url');
var location = url.parse(hostname);
if (!location.protocol || /\./.test(location.protocol)) {
hostname = 'https://' + hostname;
@ -51,15 +51,166 @@ common.parseHostname = function (hostname) {
common.apiDirectory = '_apis/telebit.cloud/index.json';
function leftpad(i, n, c) {
i = i.toString();
while (i.length < (n || 4)) {
i = (c || '0') + i;
}
return i;
}
common.otp = function getOtp() {
return leftpad(Math.round(Math.random() * 9999), 4, '0');
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
};
common.api = {};
common.api.directory = function (state, next) {
state.relayUrl = common.parseUrl(state.relay);
urequest({ url: state.relayUrl + common.apiDirectory, json: true }, function (err, resp, body) {
next(err, body);
});
};
common.api.token = function (state, handlers) {
common.api.directory(state, function (err, dir) {
// directory, requested, connect, tunnelUrl, granted, authorized
function afterDir() {
//console.log('[debug] after dir');
var otp = state.otp || state._otp || common.otp();
var authReq = state.authRequest || state._auth || {
subject: state.config.email
, subject_scheme: 'mailto'
// TODO create domains list earlier
, scope: Object.keys(state.config.servernames || {})
.concat(Object.keys(state.config.ports || {})).join(',')
, otp: otp
, hostname: os.hostname()
// Used for User-Agent
, os_type: os.type()
, os_platform: os.platform()
, os_release: os.release()
, os_arch: os.arch()
};
// backwards compat (TODO remove)
if (err || !dir || !dir.pair_request) {
//console.log('[debug] no dir, connect');
handlers.connect(authReq, function () {
/*ignore*/
handlers.end(null, function () {});
});
return;
}
state.relayHostname = common.parseHostname(state.relay);
state.wss = dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state.relayHostname) + dir.tunnel.pathname;
handlers.tunnelUrl(state.wss, function () {
//console.log('[debug] after tunnelUrl');
if (!state.config.token && state.config.secret) {
var jwt = require('jsonwebtoken');
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)
};
state.token = jwt.sign(tokenData, state.config.secret);
}
state.token = state.token || state.config.token;
if (state.token) {
//console.log('[debug] token via token or secret');
handlers.connect(state.token, function () {
handlers.end(null, function () {});
});
return;
}
// TODO sign token with own private key, including public key and thumbprint
// (much like ACME JOSE account)
// TODO do auth stuff
var pairRequestUrl = url.resolve('https://' + dir.api_host.replace(/:hostname/g, state.relayHostname), dir.pair_request.pathname);
var req = {
url: pairRequestUrl
, method: dir.pair_request.method
, json: authReq
};
var firstReq = true;
var firstReady = true;
function gotoNext(req) {
//console.log('[debug] gotoNext called');
urequest(req, function (err, resp, body) {
if (err) {
//console.log('[debug] gotoNext error');
err._request = req;
err._hint = '[telebitd.js] pair request';
handlers.error(err, function () {});
return;
}
function checkLocation() {
//console.log('[debug] checkLocation');
// pending, try again
if ('pending' === body.status && resp.headers.location) {
//console.log('[debug] pending');
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
return;
}
if ('ready' === body.status) {
//console.log('[debug] ready');
if (firstReady) {
//console.log('[debug] first ready');
firstReady = false;
state.token = body.access_token;
state.config.token = state.token;
handlers.offer(body.access_token, function () {
/*ignore*/
});
}
setTimeout(gotoNext, 2 * 1000, req);
return;
}
if ('complete' === body.status) {
//console.log('[debug] complete');
handlers.granted(null, function () {
handlers.end(null, function () {});
});
return;
}
//console.log('[debug] bad status');
var err = new Error("Bad State:" + body.status);
err._request = req;
handlers.error(err, function () {});
}
if (firstReq) {
//console.log('[debug] first req');
handlers.requested(authReq, function () {
handlers.connect(body.access_token || body.jwt, function () {
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
});
});
firstReq = false;
return;
} else {
//console.log('[debug] other req');
checkLocation();
}
});
}
gotoNext(req);
});
}
if (dir) {
handlers.directory(dir, afterDir);
} else {
afterDir();
}
});
};
try {