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("");
console.info("What relay will you be using? (press enter for default)"); console.info("What relay will you be using? (press enter for default)");
console.info(""); 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) { rl.question('relay [default: telebit.cloud]: ', function (relay) {
// TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json // TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
if (!relay) { relay = 'telebit.cloud'; } if (!relay) { relay = 'telebit.cloud'; }
answers.relay = relay.trim(); answers.relay = relay.trim();
var urlstr = parseUrl(answers.relay) + '_apis/telebit.cloud/index.json'; var urlstr = common.parseUrl(answers.relay) + common.apiDirectory;
https.get(urlstr, function (resp) { common.urequest({ url: urlstr, json: true }, function (err, resp, body) {
var body = ''; if (err) {
console.error("[Network Error] Failed to retrieve '" + urlstr + "'");
console.error(e);
askRelay(cb);
return;
}
if (200 !== resp.statusCode) { if (200 !== resp.statusCode) {
console.error("[" + resp.statusCode + " Error] Failed to retrieve '" + urlstr + "'"); console.error("[" + resp.statusCode + " Error] Failed to retrieve '" + urlstr + "'");
askRelay(cb); askRelay(cb);
return; return;
} }
resp.on('data', function (chunk) { if (Buffer.isBuffer(body) || 'object' !== typeof body) {
body += chunk.toString('utf8'); console.error("[Parse Error] Failed to retrieve '" + urlstr + "'");
}); console.error(body);
resp.on('end', function () { askRelay(cb);
try { return;
body = JSON.parse(body); }
} catch(e) { if (!body.api_host) {
console.error("[Parse Error] Failed to retrieve '" + urlstr + "'"); console.error("[API Error] API Index '" + urlstr + "' does not describe a known version of telebit.cloud");
console.error(e); console.error(e);
askRelay(cb); askRelay(cb);
return; return;
} }
if (!(body.api_host)) { if (body.pair_request) {
console.error("[API Error] API Index '" + urlstr + "' does not describe a known version of telebit.cloud"); answers._can_pair = true;
console.error(e); }
askRelay(cb); 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);
}); });
}); });
} }

View File

@ -75,12 +75,8 @@ try {
var controlServer; var controlServer;
var tun; var tun;
function serveControls() {
if (!state.config.disable) { function serveControlsHelper() {
if (state.config.relay && (state.config.token || state.config.agreeTos)) {
tun = rawTunnel();
}
}
controlServer = http.createServer(function (req, res) { controlServer = http.createServer(function (req, res) {
var opts = url.parse(req.url, true); var opts = url.parse(req.url, true);
if (opts.query._body) { if (opts.query._body) {
@ -101,13 +97,13 @@ function serveControls() {
, code: 'CONFIG' , 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.code = "AWAIT_AUTH";
dumpy.message = [ dumpy.message = [
"Check your email." "Check your email."
, "You must verify your email address to activate this device." , "You must verify your email address to activate this device."
, "" , ""
, " Login Code (if needed): " + state.otp , " Device Pairing Code: " + state.otp
].join('\n'); ].join('\n');
} }
@ -204,26 +200,32 @@ function serveControls() {
if (tun) { if (tun) {
tun.end(function () { tun.end(function () {
tun = rawTunnel(); rawTunnel(saveAndReport);
}); });
tun = null; tun = null;
setTimeout(function () { setTimeout(function () {
if (!tun) { tun = rawTunnel(); } if (!tun) {
rawTunnel(saveAndReport);
}
}, 3000); }, 3000);
} else { } else {
tun = rawTunnel(); rawTunnel(saveAndReport);
} }
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { function saveAndReport(err, _tun) {
if (err) { if (err) { throw err; }
res.statusCode = 500; tun = _tun;
res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'") fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
+ '.\nPerhaps check that the file exists and your user has permissions to write it?"}}'); if (err) {
return; 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; return;
} }
@ -350,14 +352,17 @@ function serveControls() {
listSuccess(); listSuccess();
return; return;
} }
tun = rawTunnel(); rawTunnel(function (err, _tun) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { if (err) { throw err; }
if (err) { tun = _tun;
res.statusCode = 500; fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) {
res.end('{"error":{"message":"Could not save config file. Perhaps you\'re user doesn\'t have permission?"}}'); if (err) {
return; res.statusCode = 500;
} res.end('{"error":{"message":"Could not save config file. Perhaps you\'re user doesn\'t have permission?"}}');
listSuccess(); return;
}
listSuccess();
});
}); });
return; 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 parseConfig(err, text) {
function run() { function run() {
@ -603,6 +622,7 @@ function connectTunnel() {
var tun = remote.connect({ var tun = remote.connect({
relay: state.relay relay: state.relay
, wss: state.wss
, config: state.config , config: state.config
, otp: state.otp , otp: state.otp
, sortingHat: state.sortingHat , sortingHat: state.sortingHat
@ -618,31 +638,20 @@ function connectTunnel() {
return tun; return tun;
} }
function rawTunnel() { function rawTunnel(cb) {
if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) { if (state.config.disable || !state.config.relay || !(state.config.token || state.config.agreeTos)) {
cb(null, null);
return; return;
} }
state.relay = state.config.relay; state.relay = state.config.relay;
if (!state.relay) { if (!state.relay) {
throw new Error("'" + state._confpath + "' is missing 'relay'"); cb(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);
return; return;
} }
*/
var location = url.parse(state.relay); state.relayUrl = common.parseUrl(state.relay);
if (!location.protocol || /\./.test(location.protocol)) { state.relayHostname = common.parseHostname(state.relay);
state.relay = 'wss://' + state.relay;
location = url.parse(state.relay);
}
var aud = location.hostname + (location.port ? ':' + location.port : '');
state.relay = location.protocol + '//' + aud;
if (!state.config.token && state.config.secret) { if (!state.config.token && state.config.secret) {
var jwt = require('jsonwebtoken'); var jwt = require('jsonwebtoken');
@ -662,10 +671,22 @@ function rawTunnel() {
} }
state.token = state.token || state.config.token; state.token = state.token || state.config.token;
// TODO sign token with own private key, including public key and thumbprint common.urequest({ url: state.relayUrl + common.apiDirectory, json: true }, function (err, resp, body) {
// (much like ACME JOSE account) 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); 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.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 { try {
mkdirp.sync(path.join(__dirname, '..', 'var', 'log')); mkdirp.sync(path.join(__dirname, '..', 'var', 'log'));
mkdirp.sync(path.join(__dirname, '..', 'var', 'run')); mkdirp.sync(path.join(__dirname, '..', 'var', 'run'));

View File

@ -399,7 +399,7 @@ function _connect(state) {
} }
, onOpen: function () { , onOpen: function () {
console.info("[open] connected to '" + state.relay + "'"); console.info("[open] connected to '" + (state.wss || state.relay) + "'");
wsHandlers.refreshTimeout(); wsHandlers.refreshTimeout();
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout); timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout);
@ -498,8 +498,8 @@ function _connect(state) {
timeoutId = null; timeoutId = null;
var machine = Packer.create(packerHandlers); var machine = Packer.create(packerHandlers);
console.info("[connect] '" + state.relay + "'"); console.info("[connect] '" + (state.wss || state.relay) + "'");
var tunnelUrl = state.relay.replace(/\/$/, '') + '/'; // + auth; var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure }); wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
wstunneler.on('open', wsHandlers.onOpen); wstunneler.on('open', wsHandlers.onOpen);
wstunneler.on('close', wsHandlers.onClose); wstunneler.on('close', wsHandlers.onClose);