refactoring for web ui and resumable state

This commit is contained in:
AJ ONeal 2018-10-22 00:17:49 -06:00
parent 4b64490bdc
commit c5e7811028
4 changed files with 243 additions and 138 deletions

View File

@ -4,9 +4,19 @@
<title>Telebit Setup</title> <title>Telebit Setup</title>
</head> </head>
<body> <body>
<script>document.body.hidden = true;</script>
<div class="v-app"> <div class="v-app">
<h1>Telebit (Remote) Setup</h1> <h1>Telebit (Remote) Setup</h1>
<section v-if="views.flash.error">
{{ views.flash.error }}
</section>
<section v-if="views.section.loading">
Loading...
</section>
<section v-if="views.section.setup"> <section v-if="views.section.setup">
<h2>Create Account</h2> <h2>Create Account</h2>
<form v-on:submit.stop.prevent="initialize"> <form v-on:submit.stop.prevent="initialize">
@ -100,6 +110,14 @@
<pre><code>{{ init }}</code></pre> <pre><code>{{ init }}</code></pre>
</section> </section>
<section v-if="views.section.otp">
<pre><code><h2>{{ init.otp }}</h2></code></pre>
</section>
<section v-if="views.section.status">
<pre><code>{{ status }}</code></pre>
</section>
</div> </div>
<script src="/js/vue.js"></script> <script src="/js/vue.js"></script>

View File

@ -34,20 +34,17 @@ api.config = function apiConfig() {
api.status = function apiStatus() { api.status = function apiStatus() {
return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) { return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) {
var json = resp.body; var json = resp.body;
appData.status = json;
return json; return json;
}); });
}; };
api.initialize = function apiInitialize() { api.initialize = function apiInitialize() {
var opts = { var opts = {
url: "/api/init" url: "/api/xxinitxx"
, method: "POST" , method: "POST"
, headers: { , headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
, body: JSON.stringify({ , body: JSON.stringify(telebitState.config)
foo: 'bar'
})
}; };
return Telebit.reqLocalAsync(opts).then(function (resp) { return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body; var json = resp.body;
@ -59,6 +56,47 @@ api.initialize = function apiInitialize() {
}); });
}; };
function showOtp(otp, pollUrl) {
localStorage.setItem('poll_url', pollUrl);
telebitState.pollUrl = pollUrl;
appData.init.otp = otp;
changeState('otp');
}
function doConfigure() {
if (telebitState.dir.pair_request) {
telebitState._can_pair = true;
}
//
// Read config from form
//
// Create Empty Config, If Necessary
if (!telebitState.config) { telebitState.config = {}; }
if (!telebitState.config.greenlock) { telebitState.config.greenlock = {}; }
// Populate Config
if (appData.init.teletos && appData.init.letos) { telebitState.config.agreeTos = true; }
if (appData.init.relay) { telebitState.config.relay = appData.init.relay; }
if (appData.init.email) { telebitState.config.email = appData.init.email; }
if ('undefined' !== typeof appData.init.letos) { telebitState.config.greenlock.agree = appData.init.letos; }
if ('newsletter' === appData.init.notifications) {
telebitState.config.newsletter = true; telebitState.config.communityMember = true;
}
if ('important' === appData.init.notifications) { telebitState.config.communityMember = true; }
if (appData.init.acmeVersion) { telebitState.config.greenlock.version = appData.init.acmeVersion; }
if (appData.init.acmeServer) { telebitState.config.greenlock.server = appData.init.acmeServer; }
// Temporary State
telebitState._otp = Telebit.otp();
appData.init.otp = telebitState._otp;
return Telebit.authorize(telebitState, showOtp).then(function () {
console.log('1 api.init...');
return api.initialize();
});
}
// TODO test for internet connectivity (and telebit connectivity) // TODO test for internet connectivity (and telebit connectivity)
var DEFAULT_RELAY = 'telebit.cloud'; var DEFAULT_RELAY = 'telebit.cloud';
var BETA_RELAY = 'telebit.ppl.family'; var BETA_RELAY = 'telebit.ppl.family';
@ -83,9 +121,15 @@ var appData = {
, tcp: null , tcp: null
, ssh: null , ssh: null
, views: { , views: {
section: { flash: {
setup: false error: ""
}
, section: {
loading: true
, setup: false
, advanced: false , advanced: false
, otp: false
, status: false
} }
} }
}; };
@ -98,34 +142,28 @@ var appMethods = {
} }
appData.init.relay = appData.init.relay.toLowerCase(); appData.init.relay = appData.init.relay.toLowerCase();
telebitState = { relay: appData.init.relay }; telebitState = { relay: appData.init.relay };
return Telebit.api.directory(telebitState).then(function (dir) { return Telebit.api.directory(telebitState).then(function (dir) {
if (!dir.api_host) { if (!dir.api_host) {
window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service"); window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service");
return; return;
} }
telebitState.dir = dir;
// If it's one of the well-known relays
if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) {
if (!telebitState.config) { telebitState.config = {}; } return doConfigure();
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); console.error(err);
window.alert("Error: [directory] " + (err.message || JSON.stringify(err, null, 2))); window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2)));
}); });
} }
, advance: function () { , advance: function () {
console.log('2 api.init...'); return doConfigure();
return api.initialize();
} }
, productionAcme: function () { , productionAcme: function () {
console.log("prod acme:"); console.log("prod acme:");
@ -157,10 +195,26 @@ var appStates = {
, advanced: function () { , advanced: function () {
appData.views.section = { advanced: true }; appData.views.section = { advanced: true };
} }
, otp: function () {
appData.views.section = { otp: true };
}
, status: function () {
appData.views.section = { status: true };
return api.status().then(function (status) {
appData.status = status;
});
}
}; };
function changeState(newstate) { function changeState(newstate) {
location.hash = '#/' + newstate + '/'; var newhash = '#/' + newstate + '/';
if (location.hash === newhash) {
if (!telebitState.firstState) {
telebitState.firstState = true;
setState();
}
}
location.hash = newhash;
} }
window.addEventListener('hashchange', setState, false); window.addEventListener('hashchange', setState, false);
function setState(/*ev*/) { function setState(/*ev*/) {
@ -183,11 +237,52 @@ new Vue({
}); });
api.config(); api.config().then(function (config) {
api.status().then(function () { telebitState.config = config;
changeState('setup'); if (config.greenlock) {
setState(); appData.init.acmeServer = config.greenlock.server;
}
if (config.relay) {
appData.init.relay = config.relay;
}
if (config.email) {
appData.init.email = config.email;
}
if (config.agreeTos) {
appData.init.letos = config.agreeTos;
appData.init.teletos = config.agreeTos;
}
if (config._otp) {
appData.init.otp = config._otp;
}
telebitState.pollUrl = config._pollUrl || localStorage.getItem('poll_url');
if ((!config.token && !config._otp) || !config.relay || !config.email || !config.agreeTos) {
changeState('setup');
setState();
return;
}
if (!config.token && config._otp) {
changeState('otp');
setState();
// this will skip ahead as necessary
return Telebit.authorize(telebitState, showOtp).then(function () {
console.log('2 api.init...');
return api.initialize();
});
}
// TODO handle default state
changeState('status');
}).catch(function (err) {
appData.views.flash.error = err.message || JSON.stringify(err, null, 2);
}); });
window.api = api; window.api = api;
setTimeout(function () {
document.body.hidden = false;
}, 50);
}()); }());

View File

@ -11,19 +11,14 @@ if ('undefined' !== typeof Promise) {
var common = exports.TELEBIT || require('./lib/common.js'); var common = exports.TELEBIT || require('./lib/common.js');
common.authorize = common.getToken = function getToken(state) { common.authorize = common.getToken = function getToken(state, showOtp) {
state.relay = state.config.relay; state.relay = state.config.relay;
// { _otp, config: {} } // { _otp, config: {} }
return common.api.token(state, { return common.api.token(state, {
error: function (err) { error: function (err) { console.error("[Error] common.api.token handlers.error: \n", err); return PromiseA.reject(err); }
console.error("[Error] common.api.token handlers.error:");
console.error(err);
return PromiseA.reject(err);
}
, directory: function (dir) { , directory: function (dir) {
//console.log('[directory] Telebit Relay Discovered:'); /*console.log('[directory] Telebit Relay Discovered:', dir);*/
//console.log(dir);
state._apiDirectory = dir; state._apiDirectory = dir;
return PromiseA.resolve(); return PromiseA.resolve();
} }
@ -32,12 +27,13 @@ common.authorize = common.getToken = function getToken(state) {
state.wss = tunnelUrl; state.wss = tunnelUrl;
return PromiseA.resolve(); return PromiseA.resolve();
} }
, requested: function (authReq) { , requested: function (authReq, pollUrl) {
console.log("[requested] Pairing Requested"); console.log("[requested] Pairing Requested");
state.config._otp = state.config._otp = authReq.otp; state._otp = state._otp = authReq.otp;
if (!state.config.token && state._can_pair) { if (!state.config.token && state._can_pair) {
console.info("0000".replace(/0000/g, state.config._otp)); console.info("0000".replace(/0000/g, state._otp));
showOtp(authReq.otp, pollUrl);
} }
return PromiseA.resolve(); return PromiseA.resolve();
@ -47,7 +43,9 @@ common.authorize = common.getToken = function getToken(state) {
state.config.pretoken = pretoken; state.config.pretoken = pretoken;
state._connecting = true; state._connecting = true;
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config }).then(function () { // This will only be saved to the session
state.config._otp = state._otp;
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () {
console.info("waiting..."); console.info("waiting...");
return PromiseA.resolve(); return PromiseA.resolve();
}).catch(function (err) { }).catch(function (err) {
@ -59,6 +57,7 @@ common.authorize = common.getToken = function getToken(state) {
} }
, offer: function (token) { , offer: function (token) {
//console.log("[offer] Pairing Enabled by Relay"); //console.log("[offer] Pairing Enabled by Relay");
state.token = token;
state.config.token = token; state.config.token = token;
if (state._error) { if (state._error) {
return; return;
@ -77,7 +76,7 @@ common.authorize = common.getToken = function getToken(state) {
} catch(e) { } catch(e) {
console.warn("[warning] could not decode token"); console.warn("[warning] could not decode token");
} }
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config }).then(function () { return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () {
//console.log("Pairing Enabled Locally"); //console.log("Pairing Enabled Locally");
return PromiseA.resolve(); return PromiseA.resolve();
}).catch(function (err) { }).catch(function (err) {
@ -87,12 +86,9 @@ common.authorize = common.getToken = function getToken(state) {
return PromiseA.reject(err); return PromiseA.reject(err);
}); });
} }
, granted: function (/*_*/) { , granted: function (/*_*/) { /*console.log("[grant] Pairing complete!");*/ return PromiseA.resolve(); }
//console.log("[grant] Pairing complete!");
return PromiseA.resolve();
}
, end: function () { , end: function () {
return common.reqLocalAsync({ url: '/api/enable', method: 'POST', body: [] }).then(function () { return common.reqLocalAsync({ url: '/api/enable', method: 'POST', body: [], json: true }).then(function () {
console.info("Success"); console.info("Success");
// workaround for https://github.com/nodejs/node/issues/21319 // workaround for https://github.com/nodejs/node/issues/21319
@ -112,9 +108,6 @@ common.authorize = common.getToken = function getToken(state) {
// end workaround // end workaround
//parseCli(state); //parseCli(state);
}).catch(function (err) {
console.error('[end] [error]', err);
return PromiseA.reject(err);
}); });
} }
}); });

View File

@ -48,11 +48,15 @@ if ('undefined' !== typeof fetch) {
if (!opts) { opts = {}; } if (!opts) { opts = {}; }
if (opts.json && true !== opts.json) { if (opts.json && true !== opts.json) {
opts.body = opts.json; opts.body = opts.json;
opts.json = true;
} }
if (opts.json) { if (opts.json) {
if (!opts.headers) { opts.headers = {}; } if (!opts.headers) { opts.headers = {}; }
if (opts.body) { if (opts.body) {
opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Type'] = 'application/json';
if ('string' !== typeof opts.body) {
opts.body = JSON.stringify(opts.body);
}
} else { } else {
opts.headers.Accepts = 'application/json'; opts.headers.Accepts = 'application/json';
} }
@ -126,15 +130,16 @@ common.signToken = function (state) {
return jwt.sign(tokenData, state.config.secret); return jwt.sign(tokenData, state.config.secret);
}; };
common.promiseTimeout = function (ms) { common.promiseTimeout = function (ms) {
var x = new PromiseA(function (resolve) { var tok;
x._tok = setTimeout(function () { var p = new PromiseA(function (resolve) {
tok = setTimeout(function () {
resolve(); resolve();
}, ms); }, ms);
}); });
x.cancel = function () { p.cancel = function () {
clearTimeout(x._tok); clearTimeout(tok);
}; };
return x; return p;
}; };
common.api = {}; common.api = {};
common.api.directory = function (state) { common.api.directory = function (state) {
@ -145,15 +150,10 @@ 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) {
console.error('bsnthsnth');
return PromiseA.reject(err);
}); });
}; };
common.api._parseWss = function (state, dir) { common.api._parseWss = function (state, dir) {
@ -169,15 +169,63 @@ common.api.wss = function (state) {
}); });
}; };
common.api.token = function (state, handlers) { common.api.token = function (state, handlers) {
var firstReady = true;
function pollStatus(req) {
if (common.debug) { console.log('[debug] pollStatus called'); }
if (common.debug) { console.log(req); }
return common.requestAsync(req).then(function checkLocation(resp) {
var body = resp.body;
if (common.debug) { console.log('[debug] checkLocation'); }
if (common.debug) { console.log(body); }
// pending, try again
if ('pending' === body.status && resp.headers.location) {
if (common.debug) { console.log('[debug] pending'); }
return common.promiseTimeout(2 * 1000).then(function () {
return pollStatus({ url: resp.headers.location, json: true });
});
} else if ('ready' === body.status) {
if (common.debug) { console.log('[debug] ready'); }
if (firstReady) {
if (common.debug) { console.log('[debug] first ready'); }
firstReady = false;
// falls through on purpose
PromiseA.resolve(handlers.offer(body.access_token)).then(function () {
/*ignore*/
});
}
return common.promiseTimeout(2 * 1000).then(function () {
return pollStatus(req);
});
} 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.reject(err);
}
}).catch(function (err) {
if (common.debug) { console.log('[debug] pollStatus error'); }
err._request = req;
err._hint = '[telebitd.js] pair request';
return PromiseA.resolve(handlers.error(err)).then(function () {});
});
}
// directory, requested, connect, tunnelUrl, offer, granted, end // directory, requested, connect, tunnelUrl, offer, granted, end
function afterDir(dir) { function requestAuth(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);
return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(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); // TODO make token here in the browser
//state.config._token = common.signToken(state);
} }
state.token = state.token || state.config.token || state.config._token; state.token = state.token || state.config.token || state.config._token;
if (state.token) { if (state.token) {
@ -190,13 +238,13 @@ common.api.token = function (state, handlers) {
if (!dir.pair_request) { if (!dir.pair_request) {
if (common.debug) { console.log('[debug] no dir, connect'); } if (common.debug) { console.log('[debug] no dir, connect'); }
return PromiseA.resolve(handlers.error(err || new Error("No token found or generated, and no pair_request api found."))); return PromiseA.resolve(handlers.error(new Error("No token found or generated, and no pair_request api found.")));
} }
// 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 // TODO handle agree
var otp = state.config._otp; // common.otp(); var otp = state._otp; // common.otp();
var authReq = { var authReq = {
subject: state.config.email subject: state.config.email
, subject_scheme: 'mailto' , subject_scheme: 'mailto'
@ -236,88 +284,39 @@ common.api.token = function (state, handlers) {
, method: dir.pair_request.method , method: dir.pair_request.method
, json: authReq , json: authReq
}; };
var firstReq = true;
var firstReady = true;
function gotoNext(req) { return common.requestAsync(req).then(function doFirst(resp) {
if (common.debug) { console.log('[debug] gotoNext called'); } var body = resp.body;
if (common.debug) { console.log(req); } if (common.debug) { console.log('[debug] first req'); }
return common.requestAsync(req).then(function (resp) { if (!body.access_token && !body.jwt) {
var body = resp.body; return PromiseA.reject(new Error("something wrong with pre-authorization request"));
}
function checkLocation() { return PromiseA.resolve(handlers.requested(authReq, resp.headers.location)).then(function () {
if (common.debug) { console.log('[debug] checkLocation'); } return PromiseA.resolve(handlers.connect(body.access_token || body.jwt)).then(function () {
if (common.debug) { console.log(body); } var err;
// pending, try again if (!resp.headers.location) {
if ('pending' === body.status && resp.headers.location) { err = new Error("bad authentication request response");
if (common.debug) { console.log('[debug] pending'); } err._resp = resp.toJSON && resp.toJSON();
return common.promiseTimeout(2 * 1000).then(function () { return PromiseA.resolve(handlers.error(err)).then(function () {});
return gotoNext({ url: resp.headers.location, json: true });
});
} else if ('ready' === body.status) {
if (common.debug) { console.log('[debug] ready'); }
if (firstReady) {
if (common.debug) { console.log('[debug] first ready'); }
firstReady = false;
state.token = body.access_token;
state.config.token = state.token;
// falls through on purpose
PromiseA.resolve(handlers.offer(body.access_token)).then(function () {
/*ignore*/
});
}
return common.promiseTimeout(2 * 1000).then(function () {
return gotoNext(req);
});
} 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));
} }
} return common.promiseTimeout(2 * 1000).then(function () {
return pollStatus({ url: resp.headers.location, json: true });
if (firstReq) {
if (common.debug) { console.log('[debug] first req'); }
if (!body.access_token && !body.jwt) {
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;
if (!resp.headers.location) {
err = new Error("bad authentication request response");
err._resp = resp.toJSON && resp.toJSON();
return PromiseA.resolve(handlers.error(err)).then(function () {});
}
return common.promiseTimeout(2 * 1000).then(function () {
return gotoNext({ url: resp.headers.location, json: true });
});
});
}); });
} else { });
if (common.debug) { console.log('[debug] other req'); }
return checkLocation();
}
}).catch(function (err) {
if (common.debug) { console.log('[debug] gotoNext error'); }
err._request = req;
err._hint = '[telebitd.js] pair request';
return PromiseA.resolve(handlers.error(err)).then(function () {});
}); });
} }).catch(function (err) {
if (common.debug) { console.log('[debug] gotoFirst error'); }
return gotoNext(req); err._request = req;
err._hint = '[telebitd.js] pair request';
return PromiseA.resolve(handlers.error(err)).then(function () {});
});
}); });
} }
if (state.pollUrl) {
return pollStatus({ url: state.pollUrl, json: true });
}
// 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": "" } }';
return common.api.directory(state).then(function (dir) { return common.api.directory(state).then(function (dir) {
@ -331,9 +330,9 @@ common.api.token = function (state, handlers) {
}).then(function (dir) { }).then(function (dir) {
return PromiseA.resolve(handlers.directory(dir)).then(function () { return PromiseA.resolve(handlers.directory(dir)).then(function () {
console.log('[debug] [directory]', dir); console.log('[debug] [directory]', dir);
return afterDir(dir); return requestAuth(dir);
}); });
}); });
}; };
}('undefined' !== typeof module ? module.exports : window)); }('undefined' !== typeof module ? module.exports : window));