telebit.js/lib/admin/js/app.js

609 lines
16 KiB
JavaScript
Raw Normal View History

2018-10-16 02:37:07 +00:00
;(function () {
'use strict';
var Vue = window.Vue;
2018-10-18 07:52:30 +00:00
var Telebit = window.TELEBIT;
var Keypairs = window.Keypairs;
2019-05-11 20:00:17 +00:00
var ACME = window.ACME;
2018-10-16 02:37:07 +00:00
var api = {};
/*
2018-10-21 05:25:14 +00:00
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);
});
}
*/
2018-10-21 05:25:14 +00:00
2018-10-16 02:37:07 +00:00
api.config = function apiConfig() {
return Telebit.reqLocalAsync({
2019-05-12 04:03:02 +00:00
method: "GET"
, url: "/api/config"
, key: api._key
2018-10-21 05:25:14 +00:00
}).then(function (resp) {
var json = resp.body;
appData.config = json;
return json;
2018-10-16 02:37:07 +00:00
});
};
api.status = function apiStatus() {
2019-05-12 04:03:02 +00:00
return Telebit.reqLocalAsync({
method: "GET"
, url: "/api/status"
, key: api._key
}).then(function (resp) {
var json = resp.body;
return json;
2018-10-16 02:37:07 +00:00
});
};
2018-11-01 09:11:47 +00:00
api.http = function apiHttp(o) {
2018-10-23 06:44:59 +00:00
var opts = {
2019-05-12 04:03:02 +00:00
method: "POST"
, url: "/api/http"
2018-10-23 06:44:59 +00:00
, headers: { 'Content-Type': 'application/json' }
2018-11-01 09:11:47 +00:00
, json: { name: o.name, handler: o.handler, indexes: o.indexes }
2019-05-12 04:03:02 +00:00
, key: api._key
2018-10-23 06:44:59 +00:00
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
appData.initResult = json;
return json;
}).catch(function (err) {
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
});
};
api.ssh = function apiSsh(port) {
var opts = {
2019-05-12 04:03:02 +00:00
method: "POST"
, url: "/api/ssh"
2018-10-23 06:44:59 +00:00
, headers: { 'Content-Type': 'application/json' }
, json: { port: port }
2019-05-12 04:03:02 +00:00
, key: api._key
2018-10-23 06:44:59 +00:00
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
appData.initResult = json;
return json;
}).catch(function (err) {
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
});
};
2018-10-23 04:36:46 +00:00
api.enable = function apiEnable() {
2018-10-21 05:25:14 +00:00
var opts = {
2019-05-12 04:03:02 +00:00
method: "POST"
, url: "/api/enable"
2018-10-23 04:36:46 +00:00
//, headers: { 'Content-Type': 'application/json' }
2019-05-12 04:03:02 +00:00
, key: api._key
2018-10-23 04:36:46 +00:00
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
2018-10-23 06:44:59 +00:00
console.log('enable', json);
2018-10-23 04:36:46 +00:00
return json;
}).catch(function (err) {
2018-10-23 06:44:59 +00:00
window.alert("Error: [enable] " + (err.message || JSON.stringify(err, null, 2)));
2018-10-23 04:36:46 +00:00
});
};
api.disable = function apiDisable() {
var opts = {
2019-05-12 04:03:02 +00:00
method: "POST"
, url: "/api/disable"
2018-10-23 04:36:46 +00:00
//, headers: { 'Content-Type': 'application/json' }
2019-05-12 04:03:02 +00:00
, key: api._key
2018-10-21 05:25:14 +00:00
};
return Telebit.reqLocalAsync(opts).then(function (resp) {
var json = resp.body;
2018-10-23 06:44:59 +00:00
console.log('disable', json);
return json;
}).catch(function (err) {
2018-10-23 06:44:59 +00:00
window.alert("Error: [disable] " + (err.message || JSON.stringify(err, null, 2)));
2018-10-21 05:25:14 +00:00
});
};
2018-10-16 02:37:07 +00:00
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 () {
2018-10-25 05:34:12 +00:00
return changeState('status');
});
}
2018-10-18 07:11:37 +00:00
// TODO test for internet connectivity (and telebit connectivity)
var DEFAULT_RELAY = 'telebit.cloud';
var BETA_RELAY = 'telebit.ppl.family';
2018-10-21 05:25:14 +00:00
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';
2018-10-16 02:37:07 +00:00
var appData = {
2018-10-23 04:36:46 +00:00
config: {}
, status: {}
2018-10-18 07:11:37 +00:00
, init: {
teletos: true
, letos: true
, notifications: "important"
, relay: DEFAULT_RELAY
2018-10-21 05:25:14 +00:00
, telemetry: true
, acmeServer: PRODUCTION_ACME
2018-10-18 07:11:37 +00:00
}
2018-10-23 06:44:59 +00:00
, state: {}
2018-10-18 07:11:37 +00:00
, views: {
flash: {
error: ""
}
, section: {
loading: true
, setup: false
2018-10-21 05:25:14 +00:00
, advanced: false
, otp: false
, status: false
2018-10-18 07:11:37 +00:00
}
}
2018-10-23 06:44:59 +00:00
, newHttp: {}
2018-10-16 02:37:07 +00:00
};
2018-10-21 05:25:14 +00:00
var telebitState = {};
2018-10-16 02:37:07 +00:00
var appMethods = {
initialize: function () {
console.log("call initialize");
2019-05-14 08:16:45 +00:00
return requestAccountHelper().then(function (/*key*/) {
if (!appData.init.relay) {
appData.init.relay = DEFAULT_RELAY;
2018-10-18 07:52:30 +00:00
}
2019-05-14 08:16:45 +00:00
appData.init.relay = appData.init.relay.toLowerCase();
telebitState = { relay: appData.init.relay };
2019-05-14 08:16:45 +00:00
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;
}
2019-05-14 08:16:45 +00:00
telebitState.dir = dir;
// If it's one of the well-known relays
if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) {
return doConfigure();
} else {
changeState('advanced');
}
}).catch(function (err) {
console.error(err);
window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2)));
});
2018-10-18 07:52:30 +00:00
});
2018-10-18 07:11:37 +00:00
}
2018-10-21 05:25:14 +00:00
, advance: function () {
return doConfigure();
2018-10-21 05:25:14 +00:00
}
, 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);
}
2018-10-18 07:11:37 +00:00
, defaultRelay: function () {
appData.init.relay = DEFAULT_RELAY;
}
, betaRelay: function () {
appData.init.relay = BETA_RELAY;
2018-10-16 02:37:07 +00:00
}
2018-10-23 04:36:46 +00:00
, enable: function () {
api.enable();
2018-10-21 05:25:14 +00:00
}
2018-10-23 04:36:46 +00:00
, disable: function () {
api.disable();
2018-10-21 05:25:14 +00:00
}
2018-10-23 06:44:59 +00:00
, ssh: function (port) {
// -1 to disable
// 0 is auto (22)
// 1-65536
api.ssh(port || 22);
}
2018-11-01 09:11:47 +00:00
, createShare: function (sub, domain, handler) {
if (sub) {
domain = sub + '.' + domain;
}
api.http({ name: domain, handler: handler, indexes: true });
appData.newHttp = {};
}
, createHost: function (sub, domain, handler) {
if (sub) {
domain = sub + '.' + domain;
}
api.http({ name: domain, handler: handler, 'x-forwarded-for': name });
2018-10-23 06:44:59 +00:00
appData.newHttp = {};
}
, changePortForward: function (domain, port) {
2018-11-01 09:11:47 +00:00
api.http({ name: domain.name, handler: port });
2018-10-23 06:44:59 +00:00
}
, deletePortForward: function (domain) {
2018-11-01 09:11:47 +00:00
api.http({ name: domain.name, handler: 'none' });
2018-10-23 06:44:59 +00:00
}
, changePathHost: function (domain, path) {
2018-11-01 09:11:47 +00:00
api.http({ name: domain.name, handler: path });
2018-10-23 06:44:59 +00:00
}
, deletePathHost: function (domain) {
2018-11-01 09:11:47 +00:00
api.http({ name: domain.name, handler: 'none' });
2018-10-23 06:44:59 +00:00
}
2018-11-01 08:19:07 +00:00
, changeState: changeState
2018-10-21 05:25:14 +00:00
};
var appStates = {
setup: function () {
appData.views.section = { setup: true };
}
, advanced: function () {
appData.views.section = { advanced: true };
}
, otp: function () {
appData.views.section = { otp: true };
}
, status: function () {
2018-10-23 19:18:58 +00:00
function exitState() {
clearInterval(tok);
}
2018-10-23 19:03:36 +00:00
var tok = setInterval(updateStatus, 2000);
2018-10-23 04:36:46 +00:00
2018-10-23 19:03:36 +00:00
return updateStatus().then(function () {
2018-11-01 08:19:07 +00:00
appData.views.section = { status: true, status_chooser: true };
2018-10-23 19:18:58 +00:00
return exitState;
2018-10-23 19:03:36 +00:00
});
}
2018-10-16 02:37:07 +00:00
};
2018-11-01 08:19:07 +00:00
appStates.status.share = function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
appData.views.section = { status: true, status_share: true };
return updateStatus().then(function () {
return exitState;
});
};
appStates.status.host = function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
appData.views.section = { status: true, status_host: true };
return updateStatus().then(function () {
return exitState;
});
};
appStates.status.access = function () {
function exitState() {
clearInterval(tok);
}
var tok = setInterval(updateStatus, 2000);
appData.views.section = { status: true, status_access: true };
return updateStatus().then(function () {
return exitState;
});
};
function updateStatus() {
return api.status().then(function (status) {
if (status.error) {
appData.views.flash.error = status.error.message || JSON.stringify(status.error, null, 2);
}
var wilddomains = [];
var rootdomains = [];
var subdomains = [];
var directories = [];
var portforwards = [];
var free = [];
appData.status = status;
if ('maybe' === status.ssh_requests_password) {
appData.status.ssh_active = false;
} else {
appData.status.ssh_active = true;
if ('yes' === status.ssh_requests_password) {
appData.status.ssh_insecure = true;
}
}
if ('yes' === status.ssh_password_authentication) {
appData.status.ssh_insecure = true;
}
if ('yes' === status.ssh_permit_root_login) {
appData.status.ssh_insecure = true;
}
// only update what's changed
if (appData.state.ssh !== appData.status.ssh) {
appData.state.ssh = appData.status.ssh;
}
if (appData.state.ssh_insecure !== appData.status.ssh_insecure) {
appData.state.ssh_insecure = appData.status.ssh_insecure;
}
if (appData.state.ssh_active !== appData.status.ssh_active) {
appData.state.ssh_active = appData.status.ssh_active;
}
Object.keys(appData.status.servernames).forEach(function (k) {
var s = appData.status.servernames[k];
s.name = k;
if (s.wildcard) { wilddomains.push(s); }
if (!s.sub && !s.wildcard) { rootdomains.push(s); }
if (s.sub) { subdomains.push(s); }
if (s.handler) {
if (s.handler.toString() === parseInt(s.handler, 10).toString()) {
s._port = s.handler;
portforwards.push(s);
} else {
s.path = s.handler;
directories.push(s);
}
} else {
free.push(s);
}
});
appData.status.portForwards = portforwards;
appData.status.pathHosting = directories;
appData.status.wildDomains = wilddomains;
appData.newHttp.name = (appData.status.wildDomains[0] || {}).name;
appData.state.ssh = (appData.status.ssh > 0) && appData.status.ssh || undefined;
});
}
2018-10-16 02:37:07 +00:00
2018-10-21 05:25:14 +00:00
function changeState(newstate) {
var newhash = '#/' + newstate + '/';
if (location.hash === newhash) {
if (!telebitState.firstState) {
telebitState.firstState = true;
setState();
}
}
location.hash = newhash;
2018-10-21 05:25:14 +00:00
}
2018-10-23 04:36:46 +00:00
/*globals Promise*/
2018-10-21 05:25:14 +00:00
window.addEventListener('hashchange', setState, false);
function setState(/*ev*/) {
//ev.oldURL
//ev.newURL
2018-10-23 04:36:46 +00:00
if (appData.exit) {
2018-11-01 08:19:07 +00:00
console.log('previous state exiting');
2018-10-23 04:36:46 +00:00
appData.exit.then(function (exit) {
2018-11-01 09:11:47 +00:00
if ('function' === typeof exit) {
2018-10-23 04:36:46 +00:00
exit();
}
});
}
2018-11-01 08:19:07 +00:00
var parts = location.hash.substr(1).replace(/^\//, '').replace(/\/$/, '').split('/').filter(Boolean);
2018-10-21 05:25:14 +00:00
var fn = appStates;
parts.forEach(function (s) {
console.log("state:", s);
fn = fn[s];
});
2018-10-23 04:36:46 +00:00
appData.exit = Promise.resolve(fn());
2018-10-21 05:25:14 +00:00
//appMethods.states[newstate]();
}
2018-10-23 04:36:46 +00:00
function msToHumanReadable(ms) {
var uptime = ms;
var uptimed = uptime / 1000;
var minute = 60;
var hour = 60 * minute;
var day = 24 * hour;
var days = 0;
var times = [];
while (uptimed > day) {
uptimed -= day;
days += 1;
}
times.push(days + " days ");
var hours = 0;
while (uptimed > hour) {
uptimed -= hour;
hours += 1;
}
times.push(hours.toString().padStart(2, "0") + " h ");
var minutes = 0;
while (uptimed > minute) {
uptimed -= minute;
minutes += 1;
}
times.push(minutes.toString().padStart(2, "0") + " m ");
var seconds = Math.round(uptimed);
times.push(seconds.toString().padStart(2, "0") + " s ");
return times.join('');
}
2018-10-16 02:37:07 +00:00
new Vue({
el: ".v-app"
, data: appData
2018-10-23 04:36:46 +00:00
, computed: {
statusProctime: function () {
return msToHumanReadable(this.status.proctime);
}
, statusRuntime: function () {
return msToHumanReadable(this.status.runtime);
}
, statusUptime: function () {
return msToHumanReadable(this.status.uptime);
}
}
2018-10-16 02:37:07 +00:00
, methods: appMethods
});
2019-05-14 08:16:45 +00:00
function requestAccountHelper() {
function reset() {
changeState('setup');
setState();
}
return new Promise(function (resolve) {
appData.init.email = localStorage.getItem('email');
if (!appData.init.email) {
// don't resolve
reset();
return;
}
2019-05-14 08:16:45 +00:00
return requestAccount(appData.init.email).then(function (key) {
if (!key) { throw new Error("[SANITY] Error: completed without key"); }
resolve(key);
}).catch(function (err) {
appData.init.email = "";
localStorage.removeItem('email');
console.error(err);
window.alert("something went wrong");
// don't resolve
reset();
});
});
}
function run() {
return requestAccountHelper().then(function (key) {
api._key = key;
// TODO create session instance of Telebit
Telebit._key = key;
// 😁 1. Get ACME directory
// 😁 2. Fetch ACME account
// 3. Test if account has access
// 4. Show command line auth instructions to auth
// 😁 5. Sign requests / use JWT
// 😁 6. Enforce token required for config, status, etc
// 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
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;
}
2018-10-21 05:25:14 +00:00
2019-05-14 08:16:45 +00:00
telebitState.pollUrl = config._pollUrl || localStorage.getItem('poll_url');
2019-05-14 08:16:45 +00:00
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 () {
return changeState('status');
});
}
2019-05-14 08:16:45 +00:00
// TODO handle default state
changeState('status');
}).catch(function (err) {
appData.views.flash.error = err.message || JSON.stringify(err, null, 2);
});
});
}
// TODO protect key with passphrase (or QR code?)
function getKey() {
2019-05-11 20:00:17 +00:00
var jwk;
try {
2019-05-11 20:00:17 +00:00
jwk = JSON.parse(localStorage.getItem('key'));
} catch(e) {
// ignore
}
2019-05-11 20:00:17 +00:00
if (jwk && jwk.kid && jwk.d) {
return Promise.resolve(jwk);
}
return Keypairs.generate().then(function (pair) {
2019-05-11 20:00:17 +00:00
jwk = pair.private;
localStorage.setItem('key', JSON.stringify(jwk));
return jwk;
});
}
2019-05-14 08:16:45 +00:00
function requestAccount(email) {
2019-05-11 20:00:17 +00:00
return getKey().then(function (jwk) {
2019-05-14 08:16:45 +00:00
// creates new or returns existing
var acme = ACME.create({});
var url = window.location.protocol + '//' + window.location.host + '/acme/directory';
return acme.init(url).then(function () {
return acme.accounts.create({
agreeToTerms: function (tos) { return tos; }
, accountKeypair: { privateKeyJwk: jwk }
, email: email
}).then(function (account) {
console.log('account:');
console.log(account);
if (account.id) {
localStorage.setItem('email', email);
}
return jwk;
2019-05-11 20:00:17 +00:00
});
});
});
}
2018-10-16 02:37:07 +00:00
window.api = api;
2019-05-14 08:16:45 +00:00
run();
setTimeout(function () {
document.body.hidden = false;
}, 50);
2019-05-14 08:16:45 +00:00
// Debug
window.changeState = changeState;
2018-10-16 02:37:07 +00:00
}());