diff --git a/bin/telebit.js b/bin/telebit.js index 6093748..2784f28 100755 --- a/bin/telebit.js +++ b/bin/telebit.js @@ -135,56 +135,40 @@ function askForConfig(answers, mainCb) { console.info(""); console.info("What relay will you be using? (press enter for default)"); console.info(""); - function parseUrl(hostname) { - var url = require('url'); - var location = url.parse(hostname); - if (!location.protocol || /\./.test(location.protocol)) { - hostname = 'https://' + hostname; - location = url.parse(hostname); - } - hostname = location.hostname + (location.port ? ':' + location.port : ''); - hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname; - return hostname; - } + rl.question('relay [default: telebit.cloud]: ', function (relay) { // TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json if (!relay) { relay = 'telebit.cloud'; } answers.relay = relay.trim(); - var urlstr = parseUrl(answers.relay) + '_apis/telebit.cloud/index.json'; - https.get(urlstr, function (resp) { - var body = ''; + var urlstr = common.parseUrl(answers.relay) + common.apiDirectory; + common.urequest({ url: urlstr, json: true }, function (err, resp, body) { + if (err) { + console.error("[Network Error] Failed to retrieve '" + urlstr + "'"); + console.error(e); + askRelay(cb); + return; + } if (200 !== resp.statusCode) { console.error("[" + resp.statusCode + " Error] Failed to retrieve '" + urlstr + "'"); askRelay(cb); return; } - resp.on('data', function (chunk) { - body += chunk.toString('utf8'); - }); - resp.on('end', function () { - try { - body = JSON.parse(body); - } catch(e) { - console.error("[Parse Error] Failed to retrieve '" + urlstr + "'"); - console.error(e); - askRelay(cb); - return; - } - if (!(body.api_host)) { - console.error("[API Error] API Index '" + urlstr + "' does not describe a known version of telebit.cloud"); - console.error(e); - askRelay(cb); - return; - } - if (body.pair_request) { - answers._can_pair = true; - } - cb(); - }); - }).on('error', function (e) { - console.error("[Network Error] Failed to retrieve '" + urlstr + "'"); - console.error(e); - askRelay(cb); + if (Buffer.isBuffer(body) || 'object' !== typeof body) { + console.error("[Parse Error] Failed to retrieve '" + urlstr + "'"); + console.error(body); + askRelay(cb); + return; + } + if (!body.api_host) { + console.error("[API Error] API Index '" + urlstr + "' does not describe a known version of telebit.cloud"); + console.error(e); + askRelay(cb); + return; + } + if (body.pair_request) { + answers._can_pair = true; + } + cb(); }); }); } diff --git a/bin/telebitd.js b/bin/telebitd.js index 793bb84..d2ba805 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -75,12 +75,8 @@ try { var controlServer; var tun; -function serveControls() { - if (!state.config.disable) { - if (state.config.relay && (state.config.token || state.config.agreeTos)) { - tun = rawTunnel(); - } - } + +function serveControlsHelper() { controlServer = http.createServer(function (req, res) { var opts = url.parse(req.url, true); if (opts.query._body) { @@ -101,13 +97,13 @@ function serveControls() { , code: 'CONFIG' }; - if (/\btelebit\.cloud\b/i.test(state.config.relay) && state.config.email && !state.token) { + if (state._can_pair && state.config.email && !state.token) { dumpy.code = "AWAIT_AUTH"; dumpy.message = [ "Check your email." , "You must verify your email address to activate this device." , "" - , " Login Code (if needed): " + state.otp + , " Device Pairing Code: " + state.otp ].join('\n'); } @@ -204,26 +200,32 @@ function serveControls() { if (tun) { tun.end(function () { - tun = rawTunnel(); + rawTunnel(saveAndReport); }); tun = null; setTimeout(function () { - if (!tun) { tun = rawTunnel(); } + if (!tun) { + rawTunnel(saveAndReport); + } }, 3000); } else { - tun = rawTunnel(); + rawTunnel(saveAndReport); } - fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { - if (err) { - res.statusCode = 500; - res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'") - + '.\nPerhaps check that the file exists and your user has permissions to write it?"}}'); - return; - } + function saveAndReport(err, _tun) { + if (err) { throw err; } + tun = _tun; + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + if (err) { + res.statusCode = 500; + res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'") + + '.\nPerhaps check that the file exists and your user has permissions to write it?"}}'); + return; + } - listSuccess(); - }); + listSuccess(); + }); + } return; } @@ -350,14 +352,17 @@ function serveControls() { listSuccess(); return; } - tun = rawTunnel(); - fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { - if (err) { - res.statusCode = 500; - res.end('{"error":{"message":"Could not save config file. Perhaps you\'re user doesn\'t have permission?"}}'); - return; - } - listSuccess(); + rawTunnel(function (err, _tun) { + if (err) { throw err; } + tun = _tun; + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + if (err) { + res.statusCode = 500; + res.end('{"error":{"message":"Could not save config file. Perhaps you\'re user doesn\'t have permission?"}}'); + return; + } + listSuccess(); + }); }); return; } @@ -409,6 +414,20 @@ function serveControls() { }); } +function serveControls() { + if (!state.config.disable) { + if (state.config.relay && (state.config.token || state.config.agreeTos)) { + rawTunnel(function (err, _tun) { + if (err) { throw err; } + tun = _tun; + serveControlsHelper(); + }); + return; + } + } + serveControlsHelper(); +} + function parseConfig(err, text) { function run() { @@ -603,6 +622,7 @@ function connectTunnel() { var tun = remote.connect({ relay: state.relay + , wss: state.wss , config: state.config , otp: state.otp , sortingHat: state.sortingHat @@ -618,31 +638,20 @@ function connectTunnel() { return tun; } -function rawTunnel() { +function rawTunnel(cb) { if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) { + cb(null, null); return; } state.relay = state.config.relay; if (!state.relay) { - throw new Error("'" + state._confpath + "' is missing 'relay'"); - } - - /* - if (!(state.config.secret || state.config.token)) { - console.error("You must use --secret or --token with --relay"); - process.exit(1); + cb(new Error("'" + state._confpath + "' is missing 'relay'")); return; } - */ - var location = url.parse(state.relay); - if (!location.protocol || /\./.test(location.protocol)) { - state.relay = 'wss://' + state.relay; - location = url.parse(state.relay); - } - var aud = location.hostname + (location.port ? ':' + location.port : ''); - state.relay = location.protocol + '//' + aud; + state.relayUrl = common.parseUrl(state.relay); + state.relayHostname = common.parseHostname(state.relay); if (!state.config.token && state.config.secret) { var jwt = require('jsonwebtoken'); @@ -662,10 +671,22 @@ function rawTunnel() { } state.token = state.token || state.config.token; - // TODO sign token with own private key, including public key and thumbprint - // (much like ACME JOSE account) + common.urequest({ url: state.relayUrl + common.apiDirectory, json: true }, function (err, resp, body) { + state._apiDirectory = body; + state.wss = body.tunnel.method + '://' + body.api_host.replace(/:hostname/g, state.relayHostname) + body.tunnel.pathname - return connectTunnel(); + if (token) { + cb(null, connectTunnel()); + return; + } + + // TODO sign token with own private key, including public key and thumbprint + // (much like ACME JOSE account) + + // TODO do auth stuff + + cb(null, connectTunnel()); + }); } require('fs').readFile(confpath, 'utf8', parseConfig); diff --git a/lib/cli-common.js b/lib/cli-common.js index 8238e10..dd15b18 100644 --- a/lib/cli-common.js +++ b/lib/cli-common.js @@ -26,6 +26,69 @@ common.pipename = function (config, newApi) { }; common.DEFAULT_SOCK_NAME = path.join(homedir, localshare, 'var', 'run', 'telebit.sock'); +common.parseUrl = function (hostname) { + var url = require('url'); + var location = url.parse(hostname); + if (!location.protocol || /\./.test(location.protocol)) { + hostname = 'https://' + hostname; + location = url.parse(hostname); + } + hostname = location.hostname + (location.port ? ':' + location.port : ''); + hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname; + return hostname; +}; + +common.apiDirectory = '_apis/telebit.cloud/index.json'; +common.urequest = function (opts, cb) { + // request.js behavior: + // encoding: null + json ? unknown + // json => attempt to parse, fail silently + // encoding => buffer.toString(encoding) + // null === encoding => Buffer.concat(buffers) + https.get(opts, function (resp) { + var encoding = opts.encoding; + if (null === encoding) { + resp._body = []; + } else { + resp._body = ''; + } + if (!resp.headers['content-length'] || 0 === parseInt(resp.headers['content-length'], 10)) { + cb(resp); + } + resp._bodyLength = 0; + resp.on('data', function (chunk) { + if ('string' === typeof resp.body) { + resp.body += chunk.toString(encoding); + } else { + resp._body.push(chunk); + resp._bodyLength += chunk.length; + } + }); + resp.on('end', function () { + if ('string' !== typeof resp.body) { + if (1 === resp._body.length) { + resp.body = resp._body[0]; + } else { + resp.body = Buffer.concat(resp._body, resp._bodyLength); + } + resp._body = null; + } + if (opts.json && 'string' === typeof resp.body) { + // TODO I would parse based on Content-Type + // but request.js doesn't do that. + try { + resp.body = JSON.parse(resp.body); + } catch(e) { + // ignore + } + } + cb(null, resp, resp.body); + }); + }).on('error', function (e) { + cb(e); + }); +}; + try { mkdirp.sync(path.join(__dirname, '..', 'var', 'log')); mkdirp.sync(path.join(__dirname, '..', 'var', 'run')); diff --git a/lib/remote.js b/lib/remote.js index 7000cd6..3af3709 100644 --- a/lib/remote.js +++ b/lib/remote.js @@ -399,7 +399,7 @@ function _connect(state) { } , onOpen: function () { - console.info("[open] connected to '" + state.relay + "'"); + console.info("[open] connected to '" + (state.wss || state.relay) + "'"); wsHandlers.refreshTimeout(); timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout); @@ -498,8 +498,8 @@ function _connect(state) { timeoutId = null; var machine = Packer.create(packerHandlers); - console.info("[connect] '" + state.relay + "'"); - var tunnelUrl = state.relay.replace(/\/$/, '') + '/'; // + auth; + console.info("[connect] '" + (state.wss || state.relay) + "'"); + var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth; wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure }); wstunneler.on('open', wsHandlers.onOpen); wstunneler.on('close', wsHandlers.onClose);