From c5e78110288552d77adf5f7972d7dd531e02e8ef Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 22 Oct 2018 00:17:49 -0600 Subject: [PATCH] refactoring for web ui and resumable state --- lib/admin/index.html | 18 ++++ lib/admin/js/app.js | 147 ++++++++++++++++++++++----- lib/admin/js/telebit-token.js | 35 +++---- lib/admin/js/telebit.js | 181 +++++++++++++++++----------------- 4 files changed, 243 insertions(+), 138 deletions(-) diff --git a/lib/admin/index.html b/lib/admin/index.html index c63b5d1..b10cbb7 100644 --- a/lib/admin/index.html +++ b/lib/admin/index.html @@ -4,9 +4,19 @@ Telebit Setup + +

Telebit (Remote) Setup

+
+ {{ views.flash.error }} +
+ +
+ Loading... +
+

Create Account

@@ -100,6 +110,14 @@
{{ init }}
+
+

{{ init.otp }}

+
+ +
+
{{ status }}
+
+
diff --git a/lib/admin/js/app.js b/lib/admin/js/app.js index f937748..2cfedd5 100644 --- a/lib/admin/js/app.js +++ b/lib/admin/js/app.js @@ -34,20 +34,17 @@ api.config = function apiConfig() { api.status = function apiStatus() { return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) { var json = resp.body; - appData.status = json; return json; }); }; api.initialize = function apiInitialize() { var opts = { - url: "/api/init" + url: "/api/xxinitxx" , method: "POST" , headers: { 'Content-Type': 'application/json' } - , body: JSON.stringify({ - foo: 'bar' - }) + , body: JSON.stringify(telebitState.config) }; return Telebit.reqLocalAsync(opts).then(function (resp) { 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) var DEFAULT_RELAY = 'telebit.cloud'; var BETA_RELAY = 'telebit.ppl.family'; @@ -83,9 +121,15 @@ var appData = { , tcp: null , ssh: null , views: { - section: { - setup: false + flash: { + error: "" + } + , section: { + loading: true + , setup: false , advanced: false + , otp: false + , status: false } } }; @@ -98,34 +142,28 @@ var appMethods = { } appData.init.relay = appData.init.relay.toLowerCase(); telebitState = { relay: appData.init.relay }; + return Telebit.api.directory(telebitState).then(function (dir) { if (!dir.api_host) { window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service"); return; } + + telebitState.dir = dir; + + // If it's one of the well-known relays if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { - 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))); - }); + return doConfigure(); } else { changeState('advanced'); } }).catch(function (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 () { - console.log('2 api.init...'); - return api.initialize(); + return doConfigure(); } , productionAcme: function () { console.log("prod acme:"); @@ -157,10 +195,26 @@ var appStates = { , advanced: function () { 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) { - 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); function setState(/*ev*/) { @@ -183,11 +237,52 @@ new Vue({ }); -api.config(); -api.status().then(function () { - changeState('setup'); - setState(); +api.config().then(function (config) { + telebitState.config = config; + if (config.greenlock) { + 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; + +setTimeout(function () { + document.body.hidden = false; +}, 50); + }()); diff --git a/lib/admin/js/telebit-token.js b/lib/admin/js/telebit-token.js index 2cb3204..0c3fcfe 100644 --- a/lib/admin/js/telebit-token.js +++ b/lib/admin/js/telebit-token.js @@ -11,19 +11,14 @@ if ('undefined' !== typeof Promise) { 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; // { _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); - } + error: function (err) { console.error("[Error] common.api.token handlers.error: \n", err); return PromiseA.reject(err); } , directory: function (dir) { - //console.log('[directory] Telebit Relay Discovered:'); - //console.log(dir); + /*console.log('[directory] Telebit Relay Discovered:', dir);*/ state._apiDirectory = dir; return PromiseA.resolve(); } @@ -32,12 +27,13 @@ common.authorize = common.getToken = function getToken(state) { state.wss = tunnelUrl; return PromiseA.resolve(); } - , requested: function (authReq) { + , requested: function (authReq, pollUrl) { 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) { - console.info("0000".replace(/0000/g, state.config._otp)); + console.info("0000".replace(/0000/g, state._otp)); + showOtp(authReq.otp, pollUrl); } return PromiseA.resolve(); @@ -47,7 +43,9 @@ common.authorize = common.getToken = function getToken(state) { state.config.pretoken = pretoken; 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..."); return PromiseA.resolve(); }).catch(function (err) { @@ -59,6 +57,7 @@ common.authorize = common.getToken = function getToken(state) { } , offer: function (token) { //console.log("[offer] Pairing Enabled by Relay"); + state.token = token; state.config.token = token; if (state._error) { return; @@ -77,7 +76,7 @@ common.authorize = common.getToken = function getToken(state) { } catch(e) { 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"); return PromiseA.resolve(); }).catch(function (err) { @@ -87,12 +86,9 @@ common.authorize = common.getToken = function getToken(state) { return PromiseA.reject(err); }); } - , granted: function (/*_*/) { - //console.log("[grant] Pairing complete!"); - return PromiseA.resolve(); - } + , granted: function (/*_*/) { /*console.log("[grant] Pairing complete!");*/ return PromiseA.resolve(); } , 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"); // workaround for https://github.com/nodejs/node/issues/21319 @@ -112,9 +108,6 @@ common.authorize = common.getToken = function getToken(state) { // end workaround //parseCli(state); - }).catch(function (err) { - console.error('[end] [error]', err); - return PromiseA.reject(err); }); } }); diff --git a/lib/admin/js/telebit.js b/lib/admin/js/telebit.js index e6c8907..d0ab85c 100644 --- a/lib/admin/js/telebit.js +++ b/lib/admin/js/telebit.js @@ -48,11 +48,15 @@ if ('undefined' !== typeof fetch) { if (!opts) { opts = {}; } if (opts.json && true !== opts.json) { opts.body = opts.json; + opts.json = true; } if (opts.json) { if (!opts.headers) { opts.headers = {}; } if (opts.body) { opts.headers['Content-Type'] = 'application/json'; + if ('string' !== typeof opts.body) { + opts.body = JSON.stringify(opts.body); + } } else { opts.headers.Accepts = 'application/json'; } @@ -126,15 +130,16 @@ common.signToken = function (state) { return jwt.sign(tokenData, state.config.secret); }; common.promiseTimeout = function (ms) { - var x = new PromiseA(function (resolve) { - x._tok = setTimeout(function () { + var tok; + var p = new PromiseA(function (resolve) { + tok = setTimeout(function () { resolve(); }, ms); }); - x.cancel = function () { - clearTimeout(x._tok); + p.cancel = function () { + clearTimeout(tok); }; - return x; + return p; }; common.api = {}; common.api.directory = function (state) { @@ -145,15 +150,10 @@ common.api.directory = function (state) { if (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) { - console.error('123aaaaaaaaabsnthsnth'); var dir = resp.body; state._relays[state._relayUrl] = dir; return dir; - }).catch(function (err) { - console.error('bsnthsnth'); - return PromiseA.reject(err); }); }; common.api._parseWss = function (state, dir) { @@ -169,15 +169,63 @@ common.api.wss = function (state) { }); }; 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 - function afterDir(dir) { + function requestAuth(dir) { if (common.debug) { console.log('[debug] after dir'); } state.wss = common.api._parseWss(state, dir); return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(function () { if (common.debug) { console.log('[debug] after tunnelUrl'); } 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; if (state.token) { @@ -190,13 +238,13 @@ common.api.token = function (state, handlers) { if (!dir.pair_request) { 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 // (much like ACME JOSE account) // TODO handle agree - var otp = state.config._otp; // common.otp(); + var otp = state._otp; // common.otp(); var authReq = { subject: state.config.email , subject_scheme: 'mailto' @@ -236,88 +284,39 @@ common.api.token = function (state, handlers) { , method: dir.pair_request.method , json: authReq }; - var firstReq = true; - var firstReady = true; - function gotoNext(req) { - if (common.debug) { console.log('[debug] gotoNext called'); } - if (common.debug) { console.log(req); } - return common.requestAsync(req).then(function (resp) { - var body = resp.body; - - function checkLocation() { - 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 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.requestAsync(req).then(function doFirst(resp) { + var body = resp.body; + 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")); + } + return PromiseA.resolve(handlers.requested(authReq, resp.headers.location)).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 () {}); } - } - - 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 }); - }); - }); + return common.promiseTimeout(2 * 1000).then(function () { + return pollStatus({ 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 () {}); + }); }); - } - - return gotoNext(req); - + }).catch(function (err) { + if (common.debug) { console.log('[debug] gotoFirst error'); } + 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) var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }'; return common.api.directory(state).then(function (dir) { @@ -331,9 +330,9 @@ common.api.token = function (state, handlers) { }).then(function (dir) { return PromiseA.resolve(handlers.directory(dir)).then(function () { console.log('[debug] [directory]', dir); - return afterDir(dir); + return requestAuth(dir); }); }); - }; + }('undefined' !== typeof module ? module.exports : window));