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 @@
{{ 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));
});
};