diff --git a/bin/telebitd.js b/bin/telebitd.js index 4149f2d..96e9f59 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -338,9 +338,12 @@ controllers.relay = function (req, res, opts) { return; } + console.log('POST /api/relay:'); + console.log(opts.body); + console.log(); return urequestAsync(opts.body).then(function (resp) { res.setHeader('Content-Type', 'application/json'); - var resp = resp.toJSON(); + resp = resp.toJSON(); res.end(JSON.stringify(resp)); }); }; diff --git a/lib/admin/index.html b/lib/admin/index.html index 1a743f5..c63b5d1 100644 --- a/lib/admin/index.html +++ b/lib/admin/index.html @@ -104,6 +104,7 @@ + diff --git a/lib/admin/js/app.js b/lib/admin/js/app.js index a624af2..f937748 100644 --- a/lib/admin/js/app.js +++ b/lib/admin/js/app.js @@ -5,7 +5,7 @@ var Vue = window.Vue; var Telebit = window.TELEBIT; var api = {}; -/*globals AbortController*/ +/* function safeFetch(url, opts) { var controller = new AbortController(); var tok = setTimeout(function () { @@ -19,28 +19,29 @@ function safeFetch(url, opts) { clearTimeout(tok); }); } +*/ api.config = function apiConfig() { - return safeFetch("/api/config", { - method: "GET" + return Telebit.reqLocalAsync({ + url: "/api/config" + , method: "GET" }).then(function (resp) { - return resp.json().then(function (json) { - appData.config = json; - return json; - }); + var json = resp.body; + appData.config = json; + return json; }); }; api.status = function apiStatus() { - return safeFetch("/api/status", { method: "GET" }).then(function (resp) { - return resp.json().then(function (json) { - appData.status = json; - return json; - }); + return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) { + var json = resp.body; + appData.status = json; + return json; }); }; api.initialize = function apiInitialize() { var opts = { - method: "POST" + url: "/api/init" + , method: "POST" , headers: { 'Content-Type': 'application/json' } @@ -48,14 +49,13 @@ api.initialize = function apiInitialize() { 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))); - }); + return Telebit.reqLocalAsync(opts).then(function (resp) { + var json = resp.body; + 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))); }); }; @@ -104,15 +104,27 @@ var appMethods = { return; } if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) { - return api.initialize(); + if (!telebitState.config) { telebitState.config = {}; } + if (!telebitState.config.relay) { telebitState.config.relay = telebitState.relay; } + telebitState.config.email = appData.init.email; + telebitState.config._otp = Telebit.otp(); + return Telebit.authorize(telebitState).then(function () { + console.log('1 api.init...'); + return api.initialize(); + }).catch(function (err) { + console.error(err); + window.alert("Error: [authorize] " + (err.message || JSON.stringify(err, null, 2))); + }); } else { changeState('advanced'); } }).catch(function (err) { + console.error(err); window.alert("Error: [directory] " + (err.message || JSON.stringify(err, null, 2))); }); } , advance: function () { + console.log('2 api.init...'); return api.initialize(); } , productionAcme: function () { diff --git a/lib/admin/js/telebit-token.js b/lib/admin/js/telebit-token.js new file mode 100644 index 0000000..4b09245 --- /dev/null +++ b/lib/admin/js/telebit-token.js @@ -0,0 +1,123 @@ +;(function (exports) { +'use strict'; + +/* global Promise */ +var PromiseA; +if ('undefined' !== typeof Promise) { + PromiseA = Promise; +} else { + throw new Error("no Promise implementation defined"); +} + +var common = exports.TELEBIT || require('./lib/common.js'); + +common.authorize = common.getToken = function getToken(state) { + state.relay = state.config.relay; + + // { _otp, config: {} } + return common.api.token(state, { + error: function (err) { + console.error("[Error] common.api.token handlers.error:"); + console.error(err); + return PromiseA.reject(err); + } + , directory: function (dir) { + //console.log('[directory] Telebit Relay Discovered:'); + //console.log(dir); + state._apiDirectory = dir; + return PromiseA.resolve(); + } + , tunnelUrl: function (tunnelUrl) { + //console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl); + state.wss = tunnelUrl; + return PromiseA.resolve(); + } + , requested: function (authReq) { + console.log("[requested] Pairing Requested"); + state.config._otp = state.config._otp = authReq.otp; + + if (!state.config.token && state._can_pair) { + console.info("0000".replace(/0000/g, state.config._otp)); + } + + return PromiseA.resolve(); + } + , connect: function (pretoken) { + console.log("[connect] Enabling Pairing Locally..."); + state.config.pretoken = pretoken; + state._connecting = true; + + return common.reqLocalAsync({ url: '/api/config', method: 'POST', data: state.config || {} }).then(function () { + console.info("waiting..."); + return PromiseA.resolve(); + }).catch(function (err) { + state._error = err; + console.error("Error while initializing config [connect]:"); + console.error(err); + return PromiseA.reject(err); + }); + } + , offer: function (token) { + //console.log("[offer] Pairing Enabled by Relay"); + state.config.token = token; + if (state._error) { + return; + } + state._connecting = true; + try { + //require('jsonwebtoken').decode(token); + token = token.split('.'); + token[0] = token[0].replace(/_/g, '/').replace(/-/g, '+'); + while (token[0].length % 4) { token[0] += '='; } + btoa(token[0]); + token[1] = token[1].replace(/_/g, '/').replace(/-/g, '+'); + while (token[1].length % 4) { token[1] += '='; } + btoa(token[1]); + //console.log(require('jsonwebtoken').decode(token)); + } catch(e) { + console.warn("[warning] could not decode token"); + } + return common.reqLocalAsync({ url: '/api/config', method: 'POST', data: state.config }).then(function () { + //console.log("Pairing Enabled Locally"); + return PromiseA.resolve(); + }).catch(function (err) { + state._error = err; + console.error("Error while initializing config [offer]:"); + console.error(err); + return PromiseA.reject(err); + }); + } + , granted: function (/*_*/) { + //console.log("[grant] Pairing complete!"); + return PromiseA.resolve(); + } + , end: function () { + return common.reqLocalAsync({ url: '/api/enable', method: 'POST', data: [] }).then(function () { + console.info("Success"); + + // workaround for https://github.com/nodejs/node/issues/21319 + if (state._useTty) { + setTimeout(function () { + console.info("Some fun things to try first:\n"); + console.info(" ~/telebit http ~/public"); + console.info(" ~/telebit tcp 5050"); + console.info(" ~/telebit ssh auto"); + console.info(); + console.info("Press any key to continue..."); + console.info(); + process.exit(0); + }, 0.5 * 1000); + return; + } + // end workaround + + //parseCli(state); + }).catch(function (err) { + console.error('[end] [error]', err); + return PromiseA.reject(err); + }); + } + }); +}; + +}('undefined' === typeof module ? window : module.exports)); diff --git a/lib/admin/js/telebit.js b/lib/admin/js/telebit.js index 46e4da1..6747e83 100644 --- a/lib/admin/js/telebit.js +++ b/lib/admin/js/telebit.js @@ -2,6 +2,7 @@ 'use strict'; var common = exports.TELEBIT = {}; +common.debug = true; /* global Promise */ var PromiseA; @@ -14,19 +15,6 @@ if ('undefined' !== typeof Promise) { /*globals AbortController*/ 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'; - } - } - */ // funnel requests through the local server // (avoid CORS, for now) var relayOpts = { @@ -49,13 +37,6 @@ if ('undefined' !== typeof fetch) { return window.fetch(relayOpts.url, relayOpts).then(function (resp) { clearTimeout(tok); 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 }; - */ if (json.error) { return PromiseA.reject(new Error(json.error && json.error.message || JSON.stringify(json.error))); } @@ -63,8 +44,38 @@ if ('undefined' !== typeof fetch) { }); }); }; + common.reqLocalAsync = function (opts) { + if (!opts) { 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'; + } + } + var controller = new AbortController(); + var tok = setTimeout(function () { + controller.abort(); + }, 4000); + opts.signal = controller.signal; + return window.fetch(opts.url, opts).then(function (resp) { + clearTimeout(tok); + 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.reqLocalAsync = require('util').promisify(require('@coolaj86/urequest')); } common.parseUrl = function (hostname) { @@ -78,7 +89,12 @@ common.parseUrl = function (hostname) { return hostname; }; common.parseHostname = function (hostname) { - var location = new URL(hostname); + var location = {}; + try { + location = new URL(hostname); + } catch(e) { + // ignore + } if (!location.protocol || /\./.test(location.protocol)) { hostname = 'https://' + hostname; location = new URL(hostname); @@ -109,6 +125,17 @@ common.signToken = function (state) { return jwt.sign(tokenData, state.config.secret); }; +common.promiseTimeout = function (ms) { + var x = new PromiseA(function (resolve) { + x._tok = setTimeout(function () { + resolve(); + }, ms); + }); + x.cancel = function () { + clearTimeout(x._tok); + }; + return x; +}; common.api = {}; common.api.directory = function (state) { console.log('[DEBUG] state:'); @@ -118,11 +145,14 @@ common.api.directory = function (state) { if (state._relays[state._relayUrl]) { return PromiseA.resolve(state._relays[state._relayUrl]); } + console.error('aaaaaaaaabsnthsnth'); return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) { + console.error('123aaaaaaaaabsnthsnth'); var dir = resp.body; state._relays[state._relayUrl] = dir; return dir; }).catch(function (err) { + console.error('bsnthsnth'); return PromiseA.reject(err); }); }; @@ -133,18 +163,18 @@ common.api._parseWss = function (state, dir) { 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).then(function (dir) { - cb(null, common.api._parseWss(state, dir)); - }).catch(cb); +common.api.wss = function (state) { + return common.api.directory(state).then(function (dir) { + return common.api._parseWss(state, dir); + }); }; common.api.token = function (state, handlers) { // directory, requested, connect, tunnelUrl, offer, granted, end - function afterDir(err, dir) { + function afterDir(dir) { if (common.debug) { console.log('[debug] after dir'); } state.wss = common.api._parseWss(state, dir); - handlers.tunnelUrl(state.wss, function () { + return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(function () { if (common.debug) { console.log('[debug] after tunnelUrl'); } if (state.config.secret /* && !state.config.token */) { state.config._token = common.signToken(state); @@ -153,21 +183,19 @@ common.api.token = function (state, handlers) { 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 PromiseA.resolve(handlers.connect(state.token)).then(function () { + return PromiseA.resolve(handlers.end(null)); }); - return; } - // backwards compat (TODO remove) - if (err || !dir || !dir.pair_request) { + if (!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; + return PromiseA.resolve(handlers.error(err || new Error("No token found or generated, and no pair_request api found."))); } // TODO sign token with own private key, including public key and thumbprint // (much like ACME JOSE account) + // TODO handle agree var otp = state.config._otp; // common.otp(); var authReq = { subject: state.config.email @@ -187,8 +215,21 @@ common.api.token = function (state, handlers) { */ }; var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname)); + console.log('pairRequestUrl:', pairRequestUrl); + //console.log('pairRequestUrl:', JSON.stringify(pairRequestUrl.toJSON())); var req = { - url: pairRequestUrl + // WHATWG URL defines .toJSON() but, of course, it's not implemented + // because... why would we implement JavaScript objects in the DOM + // when we can have perfectly incompatible non-JS objects? + url: { + host: pairRequestUrl.host + , hostname: pairRequestUrl.hostname + , href: pairRequestUrl.href + , pathname: pairRequestUrl.pathname + , port: pairRequestUrl.port + , protocol: pairRequestUrl.protocol + , search: pairRequestUrl.search + } , method: dir.pair_request.method , json: authReq }; @@ -198,7 +239,7 @@ common.api.token = function (state, handlers) { function gotoNext(req) { if (common.debug) { console.log('[debug] gotoNext called'); } if (common.debug) { console.log(req); } - common.requestAsync(req).then(function (resp) { + return common.requestAsync(req).then(function (resp) { var body = resp.body; function checkLocation() { @@ -207,86 +248,88 @@ common.api.token = function (state, handlers) { // 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) { + return common.promiseTimeout(2 * 1000).then(function () { + return gotoNext({ url: resp.headers.location, json: true }); + }); + } else 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 () { + // falls through on purpose + PromiseA.resolve(handlers.offer(body.access_token)).then(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 common.promiseTimeout(2 * 1000).then(function () { + return gotoNext(req); }); - return; + } else if ('complete' === body.status) { + if (common.debug) { console.log('[debug] complete'); } + return PromiseA.resolve(handlers.granted(null)).then(function () { + return PromiseA.resolve(handlers.end(null)).then(function () {}); + }); + } else { + if (common.debug) { console.log('[debug] bad status'); } + var err = new Error("Bad State:" + body.status); + err._request = req; + return PromiseA.resolve(handlers.error(err)); } - - 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 () { + if (!body.access_token && !body.jwt) { + return PromiseA.reject(new Error("something wrong with pre-authorization request")); + } + firstReq = false; + return PromiseA.resolve(handlers.requested(authReq)).then(function () { + return PromiseA.resolve(handlers.connect(body.access_token || body.jwt)).then(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; + return PromiseA.resolve(handlers.error(err)).then(function () {}); } - setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true }); + return common.promiseTimeout(2 * 1000).then(function () { + return gotoNext({ url: resp.headers.location, json: true }); + }); }); }); - firstReq = false; - return; } else { if (common.debug) { console.log('[debug] other req'); } - checkLocation(); + return 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 () {}); + return PromiseA.resolve(handlers.error(err)).then(function () {}); }); } - gotoNext(req); + return 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); - }); + return common.api.directory(state).then(function (dir) { + console.log('[debug] [directory]', dir); + if (!dir.api_host) { dir = JSON.parse(failoverDir); } + return dir; }).catch(function (err) { - return afterDir(err, JSON.parse(failoverDir)); + console.warn('[warn] [directory] fetch fail, using failover'); + console.warn(err); + return JSON.parse(failoverDir); + }).then(function (dir) { + return PromiseA.resolve(handlers.directory(dir)).then(function () { + console.log('[debug] [directory]', dir); + return afterDir(dir); + }); }); };