MAJOR: Updates for Authenticated Web UI and CLI #30

Open
coolaj86 wants to merge 77 commits from next into master
5 changed files with 282 additions and 100 deletions
Showing only changes of commit e6b7ba575f - Show all commits

View File

@ -338,9 +338,12 @@ controllers.relay = function (req, res, opts) {
return; return;
} }
console.log('POST /api/relay:');
console.log(opts.body);
console.log();
return urequestAsync(opts.body).then(function (resp) { return urequestAsync(opts.body).then(function (resp) {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
var resp = resp.toJSON(); resp = resp.toJSON();
res.end(JSON.stringify(resp)); res.end(JSON.stringify(resp));
}); });
}; };

View File

@ -104,6 +104,7 @@
<script src="/js/vue.js"></script> <script src="/js/vue.js"></script>
<script src="/js/telebit.js"></script> <script src="/js/telebit.js"></script>
<script src="/js/telebit-token.js"></script>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>
</body> </body>
</html> </html>

View File

@ -5,7 +5,7 @@ var Vue = window.Vue;
var Telebit = window.TELEBIT; var Telebit = window.TELEBIT;
var api = {}; var api = {};
/*globals AbortController*/ /*
function safeFetch(url, opts) { function safeFetch(url, opts) {
var controller = new AbortController(); var controller = new AbortController();
var tok = setTimeout(function () { var tok = setTimeout(function () {
@ -19,28 +19,29 @@ function safeFetch(url, opts) {
clearTimeout(tok); clearTimeout(tok);
}); });
} }
*/
api.config = function apiConfig() { api.config = function apiConfig() {
return safeFetch("/api/config", { return Telebit.reqLocalAsync({
method: "GET" url: "/api/config"
, method: "GET"
}).then(function (resp) { }).then(function (resp) {
return resp.json().then(function (json) { var json = resp.body;
appData.config = json; appData.config = json;
return json; return json;
});
}); });
}; };
api.status = function apiStatus() { api.status = function apiStatus() {
return safeFetch("/api/status", { method: "GET" }).then(function (resp) { return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) {
return resp.json().then(function (json) { var json = resp.body;
appData.status = json; appData.status = json;
return json; return json;
});
}); });
}; };
api.initialize = function apiInitialize() { api.initialize = function apiInitialize() {
var opts = { var opts = {
method: "POST" url: "/api/init"
, method: "POST"
, headers: { , headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
@ -48,14 +49,13 @@ api.initialize = function apiInitialize() {
foo: 'bar' foo: 'bar'
}) })
}; };
return safeFetch("/api/init", opts).then(function (resp) { return Telebit.reqLocalAsync(opts).then(function (resp) {
return resp.json().then(function (json) { var json = resp.body;
appData.initResult = json; appData.initResult = json;
window.alert("Error: [success] " + JSON.stringify(json, null, 2)); window.alert("Error: [success] " + JSON.stringify(json, null, 2));
return json; return json;
}).catch(function (err) { }).catch(function (err) {
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2))); window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
});
}); });
}; };
@ -104,15 +104,27 @@ var appMethods = {
return; return;
} }
if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) {
return api.initialize(); if (!telebitState.config) { telebitState.config = {}; }
if (!telebitState.config.relay) { telebitState.config.relay = telebitState.relay; }
telebitState.config.email = appData.init.email;
telebitState.config._otp = Telebit.otp();
return Telebit.authorize(telebitState).then(function () {
console.log('1 api.init...');
return api.initialize();
}).catch(function (err) {
console.error(err);
window.alert("Error: [authorize] " + (err.message || JSON.stringify(err, null, 2)));
});
} else { } else {
changeState('advanced'); changeState('advanced');
} }
}).catch(function (err) { }).catch(function (err) {
console.error(err);
window.alert("Error: [directory] " + (err.message || JSON.stringify(err, null, 2))); window.alert("Error: [directory] " + (err.message || JSON.stringify(err, null, 2)));
}); });
} }
, advance: function () { , advance: function () {
console.log('2 api.init...');
return api.initialize(); return api.initialize();
} }
, productionAcme: function () { , productionAcme: function () {

View File

@ -0,0 +1,123 @@
;(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) {
state.relay = state.config.relay;
// { _otp, config: {} }
return common.api.token(state, {
error: function (err) {
console.error("[Error] common.api.token handlers.error:");
console.error(err);
return PromiseA.reject(err);
}
, directory: function (dir) {
//console.log('[directory] Telebit Relay Discovered:');
//console.log(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) {
console.log("[requested] Pairing Requested");
state.config._otp = state.config._otp = authReq.otp;
if (!state.config.token && state._can_pair) {
console.info("0000".replace(/0000/g, state.config._otp));
}
return PromiseA.resolve();
}
, connect: function (pretoken) {
console.log("[connect] Enabling Pairing Locally...");
state.config.pretoken = pretoken;
state._connecting = true;
return common.reqLocalAsync({ url: '/api/config', method: 'POST', data: state.config || {} }).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.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', data: state.config }).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', data: [] }).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);
}).catch(function (err) {
console.error('[end] [error]', err);
return PromiseA.reject(err);
});
}
});
};
}('undefined' === typeof module ? window : module.exports));

View File

@ -2,6 +2,7 @@
'use strict'; 'use strict';
var common = exports.TELEBIT = {}; var common = exports.TELEBIT = {};
common.debug = true;
/* global Promise */ /* global Promise */
var PromiseA; var PromiseA;
@ -14,19 +15,6 @@ if ('undefined' !== typeof Promise) {
/*globals AbortController*/ /*globals AbortController*/
if ('undefined' !== typeof fetch) { if ('undefined' !== typeof fetch) {
common.requestAsync = function (opts) { common.requestAsync = function (opts) {
/*
if (opts.json && true !== opts.json) {
opts.body = opts.json;
}
if (opts.json) {
if (!opts.headers) { opts.headers = {}; }
if (opts.body) {
opts.headers['Content-Type'] = 'application/json';
} else {
opts.headers.Accepts = 'application/json';
}
}
*/
// funnel requests through the local server // funnel requests through the local server
// (avoid CORS, for now) // (avoid CORS, for now)
var relayOpts = { var relayOpts = {
@ -49,13 +37,6 @@ if ('undefined' !== typeof fetch) {
return window.fetch(relayOpts.url, relayOpts).then(function (resp) { return window.fetch(relayOpts.url, relayOpts).then(function (resp) {
clearTimeout(tok); clearTimeout(tok);
return resp.json().then(function (json) { 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 };
*/
if (json.error) { if (json.error) {
return PromiseA.reject(new Error(json.error && json.error.message || JSON.stringify(json.error))); return PromiseA.reject(new Error(json.error && json.error.message || JSON.stringify(json.error)));
} }
@ -63,8 +44,38 @@ if ('undefined' !== typeof fetch) {
}); });
}); });
}; };
common.reqLocalAsync = function (opts) {
if (!opts) { opts = {}; }
if (opts.json && true !== opts.json) {
opts.body = opts.json;
}
if (opts.json) {
if (!opts.headers) { opts.headers = {}; }
if (opts.body) {
opts.headers['Content-Type'] = 'application/json';
} 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 { } else {
common.requestAsync = require('util').promisify(require('@coolaj86/urequest')); common.requestAsync = require('util').promisify(require('@coolaj86/urequest'));
common.reqLocalAsync = require('util').promisify(require('@coolaj86/urequest'));
} }
common.parseUrl = function (hostname) { common.parseUrl = function (hostname) {
@ -78,7 +89,12 @@ common.parseUrl = function (hostname) {
return hostname; return hostname;
}; };
common.parseHostname = function (hostname) { common.parseHostname = function (hostname) {
var location = new URL(hostname); var location = {};
try {
location = new URL(hostname);
} catch(e) {
// ignore
}
if (!location.protocol || /\./.test(location.protocol)) { if (!location.protocol || /\./.test(location.protocol)) {
hostname = 'https://' + hostname; hostname = 'https://' + hostname;
location = new URL(hostname); location = new URL(hostname);
@ -109,6 +125,17 @@ common.signToken = function (state) {
return jwt.sign(tokenData, state.config.secret); return jwt.sign(tokenData, state.config.secret);
}; };
common.promiseTimeout = function (ms) {
var x = new PromiseA(function (resolve) {
x._tok = setTimeout(function () {
resolve();
}, ms);
});
x.cancel = function () {
clearTimeout(x._tok);
};
return x;
};
common.api = {}; common.api = {};
common.api.directory = function (state) { common.api.directory = function (state) {
console.log('[DEBUG] state:'); console.log('[DEBUG] state:');
@ -118,11 +145,14 @@ common.api.directory = function (state) {
if (state._relays[state._relayUrl]) { if (state._relays[state._relayUrl]) {
return PromiseA.resolve(state._relays[state._relayUrl]); return PromiseA.resolve(state._relays[state._relayUrl]);
} }
console.error('aaaaaaaaabsnthsnth');
return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) { return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) {
console.error('123aaaaaaaaabsnthsnth');
var dir = resp.body; var dir = resp.body;
state._relays[state._relayUrl] = dir; state._relays[state._relayUrl] = dir;
return dir; return dir;
}).catch(function (err) { }).catch(function (err) {
console.error('bsnthsnth');
return PromiseA.reject(err); return PromiseA.reject(err);
}); });
}; };
@ -133,18 +163,18 @@ common.api._parseWss = function (state, dir) {
state._relayHostname = common.parseHostname(state.relay); state._relayHostname = common.parseHostname(state.relay);
return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname; return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname;
}; };
common.api.wss = function (state, cb) { common.api.wss = function (state) {
common.api.directory(state).then(function (dir) { return common.api.directory(state).then(function (dir) {
cb(null, common.api._parseWss(state, dir)); return common.api._parseWss(state, dir);
}).catch(cb); });
}; };
common.api.token = function (state, handlers) { common.api.token = function (state, handlers) {
// directory, requested, connect, tunnelUrl, offer, granted, end // directory, requested, connect, tunnelUrl, offer, granted, end
function afterDir(err, dir) { function afterDir(dir) {
if (common.debug) { console.log('[debug] after dir'); } if (common.debug) { console.log('[debug] after dir'); }
state.wss = common.api._parseWss(state, dir); state.wss = common.api._parseWss(state, dir);
handlers.tunnelUrl(state.wss, function () { return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(function () {
if (common.debug) { console.log('[debug] after tunnelUrl'); } if (common.debug) { console.log('[debug] after tunnelUrl'); }
if (state.config.secret /* && !state.config.token */) { if (state.config.secret /* && !state.config.token */) {
state.config._token = common.signToken(state); state.config._token = common.signToken(state);
@ -153,21 +183,19 @@ common.api.token = function (state, handlers) {
if (state.token) { if (state.token) {
if (common.debug) { console.log('[debug] token via token or secret'); } if (common.debug) { console.log('[debug] token via token or secret'); }
// { token, pretoken } // { token, pretoken }
handlers.connect(state.token, function () { return PromiseA.resolve(handlers.connect(state.token)).then(function () {
handlers.end(null, function () {}); return PromiseA.resolve(handlers.end(null));
}); });
return;
} }
// backwards compat (TODO remove) if (!dir.pair_request) {
if (err || !dir || !dir.pair_request) {
if (common.debug) { console.log('[debug] no dir, connect'); } if (common.debug) { console.log('[debug] no dir, connect'); }
handlers.error(new Error("No token found or generated, and no pair_request api found.")); return PromiseA.resolve(handlers.error(err || new Error("No token found or generated, and no pair_request api found.")));
return;
} }
// TODO sign token with own private key, including public key and thumbprint // TODO sign token with own private key, including public key and thumbprint
// (much like ACME JOSE account) // (much like ACME JOSE account)
// TODO handle agree
var otp = state.config._otp; // common.otp(); var otp = state.config._otp; // common.otp();
var authReq = { var authReq = {
subject: state.config.email subject: state.config.email
@ -187,8 +215,21 @@ common.api.token = function (state, handlers) {
*/ */
}; };
var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname)); 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 = { var req = {
url: pairRequestUrl // 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
, port: pairRequestUrl.port
, protocol: pairRequestUrl.protocol
, search: pairRequestUrl.search
}
, method: dir.pair_request.method , method: dir.pair_request.method
, json: authReq , json: authReq
}; };
@ -198,7 +239,7 @@ common.api.token = function (state, handlers) {
function gotoNext(req) { function gotoNext(req) {
if (common.debug) { console.log('[debug] gotoNext called'); } if (common.debug) { console.log('[debug] gotoNext called'); }
if (common.debug) { console.log(req); } if (common.debug) { console.log(req); }
common.requestAsync(req).then(function (resp) { return common.requestAsync(req).then(function (resp) {
var body = resp.body; var body = resp.body;
function checkLocation() { function checkLocation() {
@ -207,86 +248,88 @@ common.api.token = function (state, handlers) {
// pending, try again // pending, try again
if ('pending' === body.status && resp.headers.location) { if ('pending' === body.status && resp.headers.location) {
if (common.debug) { console.log('[debug] pending'); } if (common.debug) { console.log('[debug] pending'); }
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); return common.promiseTimeout(2 * 1000).then(function () {
return; return gotoNext({ url: resp.headers.location, json: true });
} });
} else if ('ready' === body.status) {
if ('ready' === body.status) {
if (common.debug) { console.log('[debug] ready'); } if (common.debug) { console.log('[debug] ready'); }
if (firstReady) { if (firstReady) {
if (common.debug) { console.log('[debug] first ready'); } if (common.debug) { console.log('[debug] first ready'); }
firstReady = false; firstReady = false;
state.token = body.access_token; state.token = body.access_token;
state.config.token = state.token; state.config.token = state.token;
handlers.offer(body.access_token, function () { // falls through on purpose
PromiseA.resolve(handlers.offer(body.access_token)).then(function () {
/*ignore*/ /*ignore*/
}); });
} }
setTimeout(gotoNext, 2 * 1000, req); return common.promiseTimeout(2 * 1000).then(function () {
return; return gotoNext(req);
}
if ('complete' === body.status) {
if (common.debug) { console.log('[debug] complete'); }
handlers.granted(null, function () {
handlers.end(null, function () {});
}); });
return; } 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.resolve(handlers.error(err));
} }
if (common.debug) { console.log('[debug] bad status'); }
var err = new Error("Bad State:" + body.status);
err._request = req;
handlers.error(err, function () {});
} }
if (firstReq) { if (firstReq) {
if (common.debug) { console.log('[debug] first req'); } if (common.debug) { console.log('[debug] first req'); }
handlers.requested(authReq, function () { if (!body.access_token && !body.jwt) {
handlers.connect(body.access_token || body.jwt, function () { return PromiseA.reject(new Error("something wrong with pre-authorization request"));
}
firstReq = false;
return PromiseA.resolve(handlers.requested(authReq)).then(function () {
return PromiseA.resolve(handlers.connect(body.access_token || body.jwt)).then(function () {
var err; var err;
if (!resp.headers.location) { if (!resp.headers.location) {
err = new Error("bad authentication request response"); err = new Error("bad authentication request response");
err._resp = resp.toJSON && resp.toJSON(); err._resp = resp.toJSON && resp.toJSON();
handlers.error(err, function () {}); return PromiseA.resolve(handlers.error(err)).then(function () {});
return;
} }
setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); return common.promiseTimeout(2 * 1000).then(function () {
return gotoNext({ url: resp.headers.location, json: true });
});
}); });
}); });
firstReq = false;
return;
} else { } else {
if (common.debug) { console.log('[debug] other req'); } if (common.debug) { console.log('[debug] other req'); }
checkLocation(); return checkLocation();
} }
}).catch(function (err) { }).catch(function (err) {
if (common.debug) { console.log('[debug] gotoNext error'); } if (common.debug) { console.log('[debug] gotoNext error'); }
err._request = req; err._request = req;
err._hint = '[telebitd.js] pair request'; err._hint = '[telebitd.js] pair request';
handlers.error(err, function () {}); return PromiseA.resolve(handlers.error(err)).then(function () {});
}); });
} }
gotoNext(req); return gotoNext(req);
}); });
} }
// backwards compat (TODO verify we can remove this) // backwards compat (TODO verify we can remove this)
var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }'; var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }';
common.api.directory(state).then(function (dir) { return common.api.directory(state).then(function (dir) {
if (!dir.api_host) { console.log('[debug] [directory]', dir);
dir = JSON.parse(failoverDir); if (!dir.api_host) { dir = JSON.parse(failoverDir); }
return afterDir(null, dir); return dir;
}
handlers.directory(dir).then(function (dir) {
return afterDir(null, dir);
}).catch(function (err) {
return PromiseA.reject(err);
});
}).catch(function (err) { }).catch(function (err) {
return afterDir(err, JSON.parse(failoverDir)); 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 afterDir(dir);
});
}); });
}; };