WIP: use API to find tunnel

This commit is contained in:
AJ ONeal 2018-06-16 01:11:02 +00:00
parent 13326a89a6
commit 8fbd49f0e6
4 changed files with 159 additions and 91 deletions

View File

@ -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();
});
});
}

View File

@ -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);

View File

@ -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'));

View File

@ -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);