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

View File

@ -98,6 +98,9 @@ function serveControlsHelper() {
, ssh: state.config.sshAuto || 'disabled' , ssh: state.config.sshAuto || 'disabled'
, code: 'CONFIG' , code: 'CONFIG'
}; };
if (state.otp) {
dumpy.device_pair_code = state.otp;
}
if (state._can_pair && state.config.email && !state.token) { if (state._can_pair && state.config.email && !state.token) {
dumpy.code = "AWAIT_AUTH"; dumpy.code = "AWAIT_AUTH";
@ -158,6 +161,7 @@ function serveControlsHelper() {
if ('undefined' !== typeof conf.agree_tos) { if ('undefined' !== typeof conf.agree_tos) {
state.config.agreeTos = 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.relay = conf.relay || state.config.relay || '';
state.config.token = conf.token || state.config.token || null; state.config.token = conf.token || state.config.token || null;
state.config.secret = conf.secret || state.config.secret || null; state.config.secret = conf.secret || state.config.secret || null;
@ -483,31 +487,93 @@ function parseConfig(err, text) {
} }
} }
function connectTunnel() { function rawTunnel(rawCb) {
function sigHandler() { if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
console.info('Received kill signal. Attempting to exit cleanly...'); rawCb(null, null);
return;
// 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();
} }
// reverse 2FA otp
process.on('SIGINT', sigHandler); state.relay = state.config.relay;
state.net = state.net || { if (!state.relay) {
createConnection: function (info, cb) { rawCb(new Error("'" + state._confpath + "' is missing 'relay'"));
// data is the hello packet / first chunk return;
// 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;
} }
};
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.greenlockConf = state.config.greenlock || {};
state.sortingHat = state.config.sortingHat; state.sortingHat = state.config.sortingHat;
@ -515,7 +581,6 @@ function connectTunnel() {
// TODO Check undefined vs false for greenlock config // TODO Check undefined vs false for greenlock config
var remote = require('../'); var remote = require('../');
console.log();
state.greenlockConfig = { state.greenlockConfig = {
version: state.greenlockConf.version || 'draft-11' version: state.greenlockConf.version || 'draft-11'
, server: state.greenlockConf.server || 'https://acme-v02.api.letsencrypt.org/directory' , 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; state.insecure = state.config.relay_ignore_invalid_certificates;
// { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig } // { relay, config, servernames, ports, sortingHat, net, insecure, token, handlers, greenlockConfig }
var tun = remote.connect({ tun = remote.connect({
relay: state.relay relay: state.relay
, wss: state.wss , wss: state.wss
, config: state.config , config: state.config
@ -554,146 +619,14 @@ function connectTunnel() {
, sortingHat: state.sortingHat , sortingHat: state.sortingHat
, net: state.net , net: state.net
, insecure: state.insecure , insecure: state.insecure
, token: state.token , token: token // instance
, servernames: state.servernames , servernames: state.servernames
, ports: state.ports , ports: state.ports
, handlers: state.handlers , handlers: state.handlers
, greenlockConfig: state.greenlockConfig , greenlockConfig: state.greenlockConfig
}); });
return tun; cb(null, 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);
});
} }
state.handlers = { 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); require('fs').readFile(confpath, 'utf8', parseConfig);
}()); }());

View File

@ -3,9 +3,11 @@
var common = module.exports; var common = module.exports;
var path = require('path'); var path = require('path');
var url = require('url');
var mkdirp = require('mkdirp'); var mkdirp = require('mkdirp');
var os = require('os'); var os = require('os');
var homedir = os.homedir(); var homedir = os.homedir();
var urequest = require('@coolaj86/urequest');
var localshare = '.local/share/telebit'; var localshare = '.local/share/telebit';
var localconf = '.config/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.DEFAULT_SOCK_NAME = path.join(homedir, localshare, 'var', 'run', 'telebit.sock');
common.parseUrl = function (hostname) { common.parseUrl = function (hostname) {
var url = require('url');
var location = url.parse(hostname); var location = url.parse(hostname);
if (!location.protocol || /\./.test(location.protocol)) { if (!location.protocol || /\./.test(location.protocol)) {
hostname = 'https://' + hostname; hostname = 'https://' + hostname;
@ -38,7 +39,6 @@ common.parseUrl = function (hostname) {
return hostname; return hostname;
}; };
common.parseHostname = function (hostname) { common.parseHostname = function (hostname) {
var url = require('url');
var location = url.parse(hostname); var location = url.parse(hostname);
if (!location.protocol || /\./.test(location.protocol)) { if (!location.protocol || /\./.test(location.protocol)) {
hostname = 'https://' + hostname; hostname = 'https://' + hostname;
@ -51,15 +51,166 @@ common.parseHostname = function (hostname) {
common.apiDirectory = '_apis/telebit.cloud/index.json'; 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() { 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 { try {