MAJOR: Updates for Authenticated Web UI and CLI #30

Open
coolaj86 wants to merge 77 commits from next into master
3 changed files with 301 additions and 163 deletions
Showing only changes of commit 9e1c9c00ca - Show all commits

View File

@ -7,7 +7,7 @@
<div class="v-app"> <div class="v-app">
<h1>Telebit (Remote) Setup</h1> <h1>Telebit (Remote) Setup</h1>
<section v-if="views.section.create"> <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">
@ -43,12 +43,23 @@
</small> </small>
<details><summary><small>Advanced</small></summary> <details><summary><small>Advanced</small></summary>
<label for="-relay">Relay:</label><input id="-relay" v-model="init.relay" type="text" placeholder="telebit.cloud">
<label for="-relay">Relay:</label>
<input id="-relay" v-model="init.relay" type="text" placeholder="telebit.cloud">
<br> <br>
<button type="button" v-on:click="defaultRelay">Use Default</button> <button type="button" v-on:click="defaultRelay">Use Default</button>
<button type="button" v-on:click="betaRelay">Use Beta</button> <button type="button" v-on:click="betaRelay">Use Beta</button>
<br> <br>
<br> <br>
<label for="-acme-server">ACME (Let's Encrypt) Server:</label>
<input id="-acme-server" v-model="init.acmeServer" type="text" placeholder="https://acme-v02.api.letsencrypt.org/directory">
<br>
<button type="button" v-on:click="productionAcme">Use Production</button>
<button type="button" v-on:click="stagingAcme">Use Staging</button>
<br>
<br>
</details> </details>
<button type="submit">Accept &amp; Continue</button> <button type="submit">Accept &amp; Continue</button>
@ -58,22 +69,32 @@
</section> </section>
<section v-if="views.section.advanced"> <section v-if="views.section.advanced">
<h2>Advanced Setup</h2> <h2>Advanced Setup for {{ init.relay }}</h2>
<form v-on:submit.stop.prevent="initialize"> <form v-on:submit.stop.prevent="advance">
<label for="-secret">Relay Secret:</label> <strong><label for="-secret">Relay Shared Secret:</label></strong>
<input id="-secret" v-model="init.secret" type="text" placeholder="ex: xxxxxxxxxxxx"> <input id="-secret" v-model="init.secret" type="text" placeholder="ex: xxxxxxxxxxxx">
<br> <br>
<strong><label for="-domains">Domains:</label></strong>
<br>
<small>(comma separated list of domains to use for http, tls, https, etc)</small>
<br>
<input id="-domains" v-model="init.domains" type="text" placeholder="ex: whatever.com, example.com">
<br>
<strong><label for="-ports">TCP Ports:</label></strong>
<br>
<small>(comman separated list of ports, excluding 80 and 443, typically port over 1024)</small>
<br>
<input id="-ports" v-model="init.ports" type="text" placeholder="ex: 5050, 3000, 8080">
<br>
<label for="-telemetry"><input id="-telemetry" v-model="init.telemetry" type="checkbox"> <label for="-telemetry"><input id="-telemetry" v-model="init.telemetry" type="checkbox">
Contribute to Telebit by sharing telemetry</label> Contribute to Telebit by sharing telemetry</label>
<br> <br>
<label for="-relay">[Advanced] Relay:</label> <button type="submit">Finish</button>
<input id="-relay" v-model="init.relay" type="text" placeholder="telebit.cloud">
<br>
<button type="submit">Accept &amp; Continue</button>
</form> </form>
<pre><code>{{ init }}</code></pre> <pre><code>{{ init }}</code></pre>

View File

@ -1,14 +1,29 @@
;(function () { ;(function () {
'use strict'; 'use strict';
console.log("hello");
var Vue = window.Vue; var Vue = window.Vue;
var Telebit = window.TELEBIT; var Telebit = window.TELEBIT;
var api = {}; 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() { 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) { return resp.json().then(function (json) {
appData.config = json; appData.config = json;
return json; return json;
@ -16,17 +31,43 @@ api.config = function apiConfig() {
}); });
}; };
api.status = function apiStatus() { 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) { return resp.json().then(function (json) {
appData.status = json; appData.status = json;
return 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) // 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';
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 = { var appData = {
config: null config: null
, status: null , status: null
@ -35,40 +76,93 @@ var appData = {
, letos: true , letos: true
, notifications: "important" , notifications: "important"
, relay: DEFAULT_RELAY , relay: DEFAULT_RELAY
, telemetry: true
, acmeServer: PRODUCTION_ACME
} }
, http: null , http: null
, tcp: null , tcp: null
, ssh: null , ssh: null
, views: { , views: {
section: { section: {
create: true setup: false
, advanced: false
} }
} }
}; };
var telebitState = {};
var appMethods = { var appMethods = {
initialize: function () { initialize: function () {
console.log("call initialize"); console.log("call initialize");
if (!appData.init.relay) { if (!appData.init.relay) {
appData.init.relay = DEFAULT_RELAY; appData.init.relay = DEFAULT_RELAY;
} }
if (DEFAULT_RELAY !== appData.init.relay) { appData.init.relay = appData.init.relay.toLowerCase();
window.alert("TODO: Custom Relay Not Implemented Yet"); telebitState = { relay: appData.init.relay };
} return Telebit.api.directory(telebitState).then(function (dir) {
Telebit.api.directory({ relay: appData.init.relay }, function (err, dir) { if (!dir.api_host) {
if (err) { window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service");
window.alert("Error:" + (err.message || JSON.stringify(err, null, 2)));
return; 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 () { , defaultRelay: function () {
appData.init.relay = DEFAULT_RELAY; appData.init.relay = DEFAULT_RELAY;
} }
, betaRelay: function () { , betaRelay: function () {
appData.init.relay = BETA_RELAY; 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({ new Vue({
el: ".v-app" el: ".v-app"
@ -76,8 +170,12 @@ new Vue({
, methods: appMethods , methods: appMethods
}); });
api.config(); api.config();
api.status(); api.status().then(function () {
changeState('setup');
setState();
});
window.api = api; window.api = api;
}()); }());

View File

@ -11,6 +11,7 @@ if ('undefined' !== typeof Promise) {
throw new Error("no Promise implementation defined"); throw new Error("no Promise implementation defined");
} }
/*globals AbortController*/
if ('undefined' !== typeof fetch) { if ('undefined' !== typeof fetch) {
common.requestAsync = function (opts) { common.requestAsync = function (opts) {
/* /*
@ -37,7 +38,16 @@ if ('undefined' !== typeof fetch) {
} }
, body: JSON.stringify(opts) , 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) { return window.fetch(relayOpts.url, relayOpts).then(function (resp) {
clearTimeout(tok);
return resp.json().then(function (json) { return resp.json().then(function (json) {
/* /*
var headers = {}; var headers = {};
@ -100,17 +110,20 @@ common.signToken = function (state) {
return jwt.sign(tokenData, state.config.secret); return jwt.sign(tokenData, state.config.secret);
}; };
common.api = {}; common.api = {};
common.api.directory = function (state, next) { common.api.directory = function (state) {
console.log('state:'); console.log('[DEBUG] state:');
console.log(state); console.log(state);
state._relayUrl = common.parseUrl(state.relay); 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; var dir = resp.body;
if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; } state._relays[state._relayUrl] = dir;
state._apiDirectory = dir; return dir;
next(null, dir);
}).catch(function (err) { }).catch(function (err) {
next(err); return PromiseA.reject(err);
}); });
}; };
common.api._parseWss = function (state, dir) { 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; 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, cb) {
common.api.directory(state, function (err, dir) { common.api.directory(state).then(function (dir) {
cb(err, common.api._parseWss(state, dir)); cb(null, common.api._parseWss(state, dir));
}); }).catch(cb);
}; };
common.api.token = function (state, handlers) { common.api.token = function (state, handlers) {
common.api.directory(state, function (err, dir) { // directory, requested, connect, tunnelUrl, offer, granted, end
// directory, requested, connect, tunnelUrl, offer, granted, end function afterDir(err, dir) {
function afterDir() { 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 () { handlers.tunnelUrl(state.wss, 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);
} }
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) {
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 () { handlers.connect(state.token, function () {
handlers.end(null, function () {}); handlers.end(null, function () {});
}); });
return; return;
} }
// backwards compat (TODO remove) // backwards compat (TODO remove)
if (err || !dir || !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.")); handlers.error(new Error("No token found or generated, and no pair_request api found."));
return; 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)
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
, subject_scheme: 'mailto' , subject_scheme: 'mailto'
// TODO create domains list earlier // TODO create domains list earlier
, scope: (state.config._servernames || Object.keys(state.config.servernames || {})) , scope: (state.config._servernames || Object.keys(state.config.servernames || {}))
.concat(state.config._ports || Object.keys(state.config.ports || {})).join(',') .concat(state.config._ports || Object.keys(state.config.ports || {})).join(',')
, otp: otp , otp: otp
// TODO make call to daemon for this info beforehand // TODO make call to daemon for this info beforehand
/* /*
, hostname: os.hostname() , hostname: os.hostname()
// Used for User-Agent // Used for User-Agent
, os_type: os.type() , os_type: os.type()
, os_platform: os.platform() , os_platform: os.platform()
, os_release: os.release() , os_release: os.release()
, os_arch: os.arch() , os_arch: os.arch()
*/ */
}; };
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));
var req = { var req = {
url: pairRequestUrl url: pairRequestUrl
, method: dir.pair_request.method , method: dir.pair_request.method
, json: authReq , json: authReq
}; };
var firstReq = true; var firstReq = true;
var firstReady = true; var firstReady = true;
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) { common.requestAsync(req).then(function (resp) {
var body = resp.body; var body = resp.body;
function checkLocation() { function checkLocation() {
if (common.debug) { console.log('[debug] checkLocation'); } if (common.debug) { console.log('[debug] checkLocation'); }
if (common.debug) { console.log(body); } if (common.debug) { console.log(body); }
// 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 }); 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;
return; 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._request = req;
err._hint = '[telebitd.js] pair request';
handlers.error(err, function () {}); 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 () {});
});
}
}); gotoNext(req);
}
});
if (dir && dir.api_host) { }
handlers.directory(dir, afterDir);
} else { // backwards compat (TODO verify we can remove this)
// backwards compat var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }';
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; common.api.directory(state).then(function (dir) {
afterDir(); 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));
}); });
}; };