From f0222baff6579a62e43c77f650296591373e9d97 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 18 Oct 2018 01:52:30 -0600 Subject: [PATCH] wip adapt common init for web setup --- lib/admin/index.html | 1 + lib/admin/js/app.js | 8 ++ lib/admin/js/telebit.js | 247 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 lib/admin/js/telebit.js diff --git a/lib/admin/index.html b/lib/admin/index.html index bf11dba..97855c8 100644 --- a/lib/admin/index.html +++ b/lib/admin/index.html @@ -80,6 +80,7 @@ + diff --git a/lib/admin/js/app.js b/lib/admin/js/app.js index 021f781..8554aa2 100644 --- a/lib/admin/js/app.js +++ b/lib/admin/js/app.js @@ -4,6 +4,7 @@ console.log("hello"); var Vue = window.Vue; +var Telebit = window.TELEBIT; var api = {}; api.config = function apiConfig() { @@ -53,6 +54,13 @@ var appMethods = { 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))); + return; + } + window.alert("Success:" + JSON.stringify(dir, null, 2)); + }); } , defaultRelay: function () { appData.init.relay = DEFAULT_RELAY; diff --git a/lib/admin/js/telebit.js b/lib/admin/js/telebit.js new file mode 100644 index 0000000..7ba8a31 --- /dev/null +++ b/lib/admin/js/telebit.js @@ -0,0 +1,247 @@ +;(function (exports) { +'use strict'; + +var common = exports.TELEBIT = {}; + +if ('undefined' !== typeof fetch) { + common.requestAsync = function (opts) { + if (opts.json && true !== opts.json) { + opts.body = opts.json; + } + if (opts.json) { + if (!opts.headers) { opts.headers = {}; } + if (opts.body) { + opts.headers['Content-Type'] = 'application/json'; + } else { + opts.headers.Accepts = 'application/json'; + } + } + return window.fetch(opts.url, opts).then(function (resp) { + return resp.json().then(function (json) { + var headers = {}; + resp.headers.forEach(function (k, v) { + headers[k] = v; + }); + return { statusCode: resp.status, headers: headers, body: json }; + }); + }); + }; +} else { + common.requestAsync = require('util').promisify(require('@coolaj86/urequest')); +} + +common.parseUrl = function (hostname) { + // add scheme, if missing + if (!/:\/\//.test(hostname)) { + hostname = 'https://' + hostname; + } + var location = new URL(hostname); + hostname = location.hostname + (location.port ? ':' + location.port : ''); + hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname; + return hostname; +}; +common.parseHostname = function (hostname) { + var location = new URL(hostname); + if (!location.protocol || /\./.test(location.protocol)) { + hostname = 'https://' + hostname; + location = new URL(hostname); + } + //hostname = location.hostname + (location.port ? ':' + location.port : ''); + //hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname; + return location.hostname; +}; + +common.apiDirectory = '_apis/telebit.cloud/index.json'; + +common.otp = function getOtp() { + return Math.round(Math.random() * 9999).toString().padStart(4, '0'); +}; +common.signToken = function (state) { + var jwt = require('jsonwebtoken'); + var tokenData = { + domains: Object.keys(state.config.servernames || {}).filter(function (name) { + return /\./.test(name); + }) + , ports: Object.keys(state.config.ports || {}).filter(function (port) { + port = parseInt(port, 10); + return port > 0 && port <= 65535; + }) + , aud: state._relayUrl + , iss: Math.round(Date.now() / 1000) + }; + + return jwt.sign(tokenData, state.config.secret); +}; +common.api = {}; +common.api.directory = function (state, next) { + console.log('state:'); + console.log(state); + state._relayUrl = common.parseUrl(state.relay); + 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); + }).catch(function (err) { + next(err); + }); +}; +common.api._parseWss = function (state, dir) { + if (!dir || !dir.api_host) { + dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; + } + state._relayHostname = common.parseHostname(state.relay); + 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.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); + + 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; + } + + // 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 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; + 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 + dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; + afterDir(); + } + }); + +}; +}('undefined' !== typeof module ? module.exports : window));