diff --git a/lib/admin/index.html b/lib/admin/index.html index f6bfa12..1a743f5 100644 --- a/lib/admin/index.html +++ b/lib/admin/index.html @@ -7,7 +7,7 @@

Telebit (Remote) Setup

-
+

Create Account

@@ -43,12 +43,23 @@
Advanced - + + +


+ + + +
+ + +
+
+
@@ -58,22 +69,32 @@
-

Advanced Setup

- +

Advanced Setup for {{ init.relay }}

+ - +
+ +
+ (comma separated list of domains to use for http, tls, https, etc) +
+ +
+ + +
+ (comman separated list of ports, excluding 80 and 443, typically port over 1024) +
+ +
+
- - -
- - +
{{ init }}
diff --git a/lib/admin/js/app.js b/lib/admin/js/app.js index 8554aa2..a624af2 100644 --- a/lib/admin/js/app.js +++ b/lib/admin/js/app.js @@ -1,14 +1,29 @@ ;(function () { 'use strict'; -console.log("hello"); - var Vue = window.Vue; var Telebit = window.TELEBIT; var api = {}; +/*globals AbortController*/ +function safeFetch(url, opts) { + var controller = new AbortController(); + var tok = setTimeout(function () { + controller.abort(); + }, 4000); + if (!opts) { + opts = {}; + } + opts.signal = controller.signal; + return window.fetch(url, opts).finally(function () { + clearTimeout(tok); + }); +} + api.config = function apiConfig() { - return window.fetch("/api/config", { method: "GET" }).then(function (resp) { + return safeFetch("/api/config", { + method: "GET" + }).then(function (resp) { return resp.json().then(function (json) { appData.config = json; return json; @@ -16,17 +31,43 @@ api.config = function apiConfig() { }); }; api.status = function apiStatus() { - return window.fetch("/api/status", { method: "GET" }).then(function (resp) { + return safeFetch("/api/status", { method: "GET" }).then(function (resp) { return resp.json().then(function (json) { appData.status = json; return json; }); }); }; +api.initialize = function apiInitialize() { + var opts = { + method: "POST" + , headers: { + 'Content-Type': 'application/json' + } + , body: JSON.stringify({ + foo: 'bar' + }) + }; + return safeFetch("/api/init", opts).then(function (resp) { + return resp.json().then(function (json) { + appData.initResult = json; + window.alert("Error: [success] " + JSON.stringify(json, null, 2)); + return json; + }).catch(function (err) { + window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2))); + }); + }); +}; // TODO test for internet connectivity (and telebit connectivity) var DEFAULT_RELAY = 'telebit.cloud'; var BETA_RELAY = 'telebit.ppl.family'; +var TELEBIT_RELAYS = [ + DEFAULT_RELAY +, BETA_RELAY +]; +var PRODUCTION_ACME = 'https://acme-v02.api.letsencrypt.org/directory'; +var STAGING_ACME = 'https://acme-staging-v02.api.letsencrypt.org/directory'; var appData = { config: null , status: null @@ -35,40 +76,93 @@ var appData = { , letos: true , notifications: "important" , relay: DEFAULT_RELAY + , telemetry: true + , acmeServer: PRODUCTION_ACME } , http: null , tcp: null , ssh: null , views: { section: { - create: true + setup: false + , advanced: false } } }; +var telebitState = {}; var appMethods = { initialize: function () { console.log("call initialize"); if (!appData.init.relay) { appData.init.relay = DEFAULT_RELAY; } - if (DEFAULT_RELAY !== appData.init.relay) { - window.alert("TODO: Custom Relay Not Implemented Yet"); - } - Telebit.api.directory({ relay: appData.init.relay }, function (err, dir) { - if (err) { - window.alert("Error:" + (err.message || JSON.stringify(err, null, 2))); + 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; } - window.alert("Success:" + JSON.stringify(dir, null, 2)); + if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { + return api.initialize(); + } else { + changeState('advanced'); + } + }).catch(function (err) { + window.alert("Error: [directory] " + (err.message || JSON.stringify(err, null, 2))); }); } +, advance: function () { + return api.initialize(); + } +, productionAcme: function () { + console.log("prod acme:"); + appData.init.acmeServer = PRODUCTION_ACME; + console.log(appData.init.acmeServer); + } +, stagingAcme: function () { + console.log("staging acme:"); + appData.init.acmeServer = STAGING_ACME; + console.log(appData.init.acmeServer); + } , defaultRelay: function () { appData.init.relay = DEFAULT_RELAY; } , betaRelay: function () { appData.init.relay = BETA_RELAY; } +, defaultRhubarb: function () { + appData.init.rhubarb = DEFAULT_RELAY; + } +, betaRhubarb: function () { + appData.init.rhubarb = BETA_RELAY; + } }; +var appStates = { + setup: function () { + appData.views.section = { setup: true }; + } +, advanced: function () { + appData.views.section = { advanced: true }; + } +}; + +function changeState(newstate) { + location.hash = '#/' + newstate + '/'; +} +window.addEventListener('hashchange', setState, false); +function setState(/*ev*/) { + //ev.oldURL + //ev.newURL + var parts = location.hash.substr(1).replace(/^\//, '').replace(/\/$/, '').split('/'); + var fn = appStates; + parts.forEach(function (s) { + console.log("state:", s); + fn = fn[s]; + }); + fn(); + //appMethods.states[newstate](); +} new Vue({ el: ".v-app" @@ -76,8 +170,12 @@ new Vue({ , methods: appMethods }); + api.config(); -api.status(); +api.status().then(function () { + changeState('setup'); + setState(); +}); window.api = api; }()); diff --git a/lib/admin/js/telebit.js b/lib/admin/js/telebit.js index bb7fe75..46e4da1 100644 --- a/lib/admin/js/telebit.js +++ b/lib/admin/js/telebit.js @@ -11,6 +11,7 @@ if ('undefined' !== typeof Promise) { throw new Error("no Promise implementation defined"); } +/*globals AbortController*/ if ('undefined' !== typeof fetch) { common.requestAsync = function (opts) { /* @@ -37,7 +38,16 @@ if ('undefined' !== typeof fetch) { } , body: JSON.stringify(opts) }; + var controller = new AbortController(); + var tok = setTimeout(function () { + controller.abort(); + }, 4000); + if (!relayOpts) { + relayOpts = {}; + } + relayOpts.signal = controller.signal; return window.fetch(relayOpts.url, relayOpts).then(function (resp) { + clearTimeout(tok); return resp.json().then(function (json) { /* var headers = {}; @@ -100,17 +110,20 @@ common.signToken = function (state) { return jwt.sign(tokenData, state.config.secret); }; common.api = {}; -common.api.directory = function (state, next) { - console.log('state:'); +common.api.directory = function (state) { + console.log('[DEBUG] state:'); console.log(state); state._relayUrl = common.parseUrl(state.relay); - common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) { + if (!state._relays) { state._relays = {}; } + if (state._relays[state._relayUrl]) { + return PromiseA.resolve(state._relays[state._relayUrl]); + } + return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) { var dir = resp.body; - if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; } - state._apiDirectory = dir; - next(null, dir); + state._relays[state._relayUrl] = dir; + return dir; }).catch(function (err) { - next(err); + return PromiseA.reject(err); }); }; common.api._parseWss = function (state, dir) { @@ -121,153 +134,159 @@ common.api._parseWss = function (state, dir) { return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname; }; common.api.wss = function (state, cb) { - common.api.directory(state, function (err, dir) { - cb(err, common.api._parseWss(state, dir)); - }); + common.api.directory(state).then(function (dir) { + cb(null, common.api._parseWss(state, dir)); + }).catch(cb); }; common.api.token = function (state, handlers) { - common.api.directory(state, function (err, dir) { - // directory, requested, connect, tunnelUrl, offer, granted, end - function afterDir() { - if (common.debug) { console.log('[debug] after dir'); } - state.wss = common.api._parseWss(state, dir); + // directory, requested, connect, tunnelUrl, offer, granted, end + function afterDir(err, dir) { + if (common.debug) { console.log('[debug] after dir'); } + state.wss = common.api._parseWss(state, dir); - handlers.tunnelUrl(state.wss, function () { - if (common.debug) { console.log('[debug] after tunnelUrl'); } - if (state.config.secret /* && !state.config.token */) { - state.config._token = common.signToken(state); - } - state.token = state.token || state.config.token || state.config._token; - if (state.token) { - if (common.debug) { console.log('[debug] token via token or secret'); } - // { token, pretoken } - handlers.connect(state.token, function () { - handlers.end(null, function () {}); - }); - return; - } + handlers.tunnelUrl(state.wss, function () { + if (common.debug) { console.log('[debug] after tunnelUrl'); } + if (state.config.secret /* && !state.config.token */) { + state.config._token = common.signToken(state); + } + state.token = state.token || state.config.token || state.config._token; + if (state.token) { + if (common.debug) { console.log('[debug] token via token or secret'); } + // { token, pretoken } + handlers.connect(state.token, function () { + handlers.end(null, function () {}); + }); + return; + } - // backwards compat (TODO remove) - if (err || !dir || !dir.pair_request) { - 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; - } + // backwards compat (TODO remove) + if (err || !dir || !dir.pair_request) { + 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; + } - // TODO sign token with own private key, including public key and thumbprint - // (much like ACME JOSE account) - var otp = state.config._otp; // common.otp(); - var authReq = { - subject: state.config.email - , subject_scheme: 'mailto' - // TODO create domains list earlier - , scope: (state.config._servernames || Object.keys(state.config.servernames || {})) - .concat(state.config._ports || Object.keys(state.config.ports || {})).join(',') - , otp: otp - // TODO make call to daemon for this info beforehand - /* - , hostname: os.hostname() - // Used for User-Agent - , os_type: os.type() - , os_platform: os.platform() - , os_release: os.release() - , os_arch: os.arch() - */ - }; - var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname)); - var req = { - url: pairRequestUrl - , method: dir.pair_request.method - , json: authReq - }; - var firstReq = true; - var firstReady = true; + // TODO sign token with own private key, including public key and thumbprint + // (much like ACME JOSE account) + var otp = state.config._otp; // common.otp(); + var authReq = { + subject: state.config.email + , subject_scheme: 'mailto' + // TODO create domains list earlier + , scope: (state.config._servernames || Object.keys(state.config.servernames || {})) + .concat(state.config._ports || Object.keys(state.config.ports || {})).join(',') + , otp: otp + // TODO make call to daemon for this info beforehand + /* + , hostname: os.hostname() + // Used for User-Agent + , os_type: os.type() + , os_platform: os.platform() + , os_release: os.release() + , os_arch: os.arch() + */ + }; + var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname)); + var req = { + url: pairRequestUrl + , 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); } - common.requestAsync(req).then(function (resp) { - var body = resp.body; + function gotoNext(req) { + if (common.debug) { console.log('[debug] gotoNext called'); } + if (common.debug) { console.log(req); } + 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'); } - setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); - return; - } - - 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; - handlers.offer(body.access_token, function () { - /*ignore*/ - }); - } - setTimeout(gotoNext, 2 * 1000, req); - return; - } - - if ('complete' === body.status) { - if (common.debug) { console.log('[debug] complete'); } - handlers.granted(null, function () { - handlers.end(null, function () {}); - }); - return; - } - - 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 (common.debug) { console.log('[debug] first req'); } - handlers.requested(authReq, function () { - handlers.connect(body.access_token || body.jwt, function () { - var err; - if (!resp.headers.location) { - err = new Error("bad authentication request response"); - err._resp = resp.toJSON && resp.toJSON(); - handlers.error(err, function () {}); - return; - } - setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); - }); - }); - firstReq = false; + 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'); } + setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); return; - } else { - if (common.debug) { console.log('[debug] other req'); } - checkLocation(); } - }).catch(function (err) { - if (common.debug) { console.log('[debug] gotoNext error'); } + + 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; + handlers.offer(body.access_token, function () { + /*ignore*/ + }); + } + setTimeout(gotoNext, 2 * 1000, req); + return; + } + + if ('complete' === body.status) { + if (common.debug) { console.log('[debug] complete'); } + handlers.granted(null, function () { + handlers.end(null, function () {}); + }); + return; + } + + if (common.debug) { console.log('[debug] bad status'); } + var err = new Error("Bad State:" + body.status); err._request = req; - err._hint = '[telebitd.js] pair request'; handlers.error(err, function () {}); - }); - } + } - gotoNext(req); + if (firstReq) { + if (common.debug) { console.log('[debug] first req'); } + handlers.requested(authReq, function () { + handlers.connect(body.access_token || body.jwt, function () { + var err; + if (!resp.headers.location) { + err = new Error("bad authentication request response"); + err._resp = resp.toJSON && resp.toJSON(); + handlers.error(err, function () {}); + return; + } + setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); + }); + }); + firstReq = false; + return; + } else { + if (common.debug) { console.log('[debug] other req'); } + checkLocation(); + } + }).catch(function (err) { + if (common.debug) { console.log('[debug] gotoNext error'); } + err._request = req; + err._hint = '[telebitd.js] pair request'; + handlers.error(err, function () {}); + }); + } - }); - } - - if (dir && dir.api_host) { - handlers.directory(dir, afterDir); - } else { - // backwards compat - dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; - afterDir(); + gotoNext(req); + + }); + } + + // backwards compat (TODO verify we can remove this) + var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }'; + common.api.directory(state).then(function (dir) { + if (!dir.api_host) { + dir = JSON.parse(failoverDir); + return afterDir(null, dir); } + handlers.directory(dir).then(function (dir) { + return afterDir(null, dir); + }).catch(function (err) { + return PromiseA.reject(err); + }); + }).catch(function (err) { + return afterDir(err, JSON.parse(failoverDir)); }); };