Compare commits

...

3 Commits

3 changed files with 425 additions and 397 deletions

View File

@ -8,11 +8,11 @@ var os = require('os');
//var url = require('url'); //var url = require('url');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var http = require('http');
//var https = require('https'); //var https = require('https');
var YAML = require('js-yaml'); var YAML = require('js-yaml');
var TOML = require('toml'); var TOML = require('toml');
var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8'));
/* /*
if ('function' !== typeof TOML.stringify) { if ('function' !== typeof TOML.stringify) {
TOML.stringify = require('json2toml'); TOML.stringify = require('json2toml');
@ -314,92 +314,186 @@ function askForConfig(state, mainCb) {
next(); next();
} }
var utils = { var RC;
request: function request(opts, fn) {
if (!opts) { opts = {}; }
var service = opts.service || 'config';
var req = http.request({
socketPath: state._ipc.path
, method: opts.method || 'GET'
, path: '/rpc/' + service
}, function (resp) {
var body = '';
function finish() { function parseConfig(err, text) {
if (200 !== resp.statusCode) { function handleConfig(err, config) {
console.warn(resp.statusCode); //console.log('CONFIG');
console.warn(body || ('get' + service + ' failed')); //console.log(config);
//cb(new Error("not okay"), body); state.config = config;
return; var verstrd = [ pkg.name + ' daemon v' + state.config.version ];
} if (state.config.version && state.config.version !== pkg.version) {
console.info(verstr.join(' '), verstrd.join(' '));
if (!body) { fn(null, null); return; }
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
fn(null, body);
}
if (resp.headers['content-length']) {
resp.on('data', function (chunk) {
body += chunk.toString();
});
resp.on('end', function () {
finish();
});
} else { } else {
finish(); console.info(verstr.join(' '));
} }
});
req.on('error', function (err) {
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
// ECONNREFUSED - leftover socket just needs to be restarted
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
if (opts._taketwo) {
console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to.");
console.error(err); console.error(err);
} else if ('ENOTSOCK' === err.code) {
console.error(err);
return; return;
} else {
console.error(err);
} }
require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) { if (err) { process.exit(101); return; }
if (err) { fn(err); return; }
opts._taketwo = true; //
setTimeout(function () { // check for init first, before anything else
utils.request(opts, fn); // because it has arguments that may help in
}, 2500); // the next steps
//
if (-1 !== argv.indexOf('init')) {
parsers.init(argv, function (err) {
if (err) {
console.error("Error while initializing config [init]:");
throw err;
}
getToken(function (err) {
if (err) {
console.error("Error while getting token [init]:");
throw err;
}
parseCli(state);
});
}); });
return; return;
} }
if ('ENOTSOCK' === err.code) {
console.error(err); if (!state.config.relay || !state.config.token) {
return; if (!state.config.relay) {
state.config.relay = 'telebit.cloud';
} }
console.error(err);
return; //console.log("question the user?", Date.now());
askForConfig(state, function (err, state) {
// no errors actually get passed, so this is just future-proofing
if (err) { throw err; }
if (!state.config.token && state._can_pair) {
state.config._otp = common.otp();
}
//console.log("done questioning:", Date.now());
if (!state.token && !state.config.token) {
if (err) {
console.error("Error while initializing config [init]:");
throw err;
}
getToken(function (err) {
if (err) {
console.error("Error while getting token [init]:");
throw err;
}
parseCli(state);
}); });
req.end(); } else {
parseCli(state);
} }
, putConfig: function putConfig(service, args, fn) { });
var req = http.request({
socketPath: state._ipc.path
, method: 'POST'
, path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args))
}, function (resp) {
function finish() {
if ('function' === typeof fn) {
fn(null, resp);
return; return;
} }
//console.log("no questioning:");
parseCli(state);
}
function parseCli(/*state*/) {
var special = [
'false', 'none', 'off', 'disable'
, 'true', 'auto', 'on', 'enable'
];
if (-1 !== argv.indexOf('init')) {
RC.request({ service: 'list', method: 'POST', data: [] }, handleRemoteRequest('list'));
return;
}
if ([ 'ssh', 'http', 'tcp' ].some(function (key) {
if (key !== argv[0]) {
return false;
}
if (argv[1]) {
if (String(argv[1]) === String(parseInt(argv[1], 10))) {
// looks like a port
argv[1] = parseInt(argv[1], 10);
} else if (/\/|\\/.test(argv[1])) {
// looks like a path
argv[1] = path.resolve(argv[1]);
// TODO make a default assignment here
} else if (-1 === special.indexOf(argv[1])) {
console.error("Not sure what you meant by '" + argv[1] + "'.");
console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'");
return true;
}
RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0]));
return true;
}
return true;
})) {
return;
}
// Two styles:
// http 3000
// http modulename
function makeRpc(key) {
if (key !== argv[0]) {
return false;
}
RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0]));
return true;
}
if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) {
return;
}
help();
process.exit(11);
}
try {
state._clientConfig = JSON.parse(text || '{}');
} catch(e1) {
try {
state._clientConfig = YAML.safeLoad(text || '{}');
} catch(e2) {
try {
state._clientConfig = TOML.parse(text || '');
} catch(e3) {
console.error(e1.message);
console.error(e2.message);
process.exit(1);
return;
}
}
}
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
RC = require('../lib/remote-control-client.js').create(state);
if (!Object.keys(state._clientConfig).length) {
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
console.info(""); console.info("");
if (200 !== resp.statusCode) { }
if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) {
if (!err || 'ENOENT' === err.code) {
//console.warn("Empty config file. Run 'telebit init' to configure.\n");
} else {
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
}
}
function handleRemoteRequest(service, fn) {
return function (err, body) {
if ('function' === typeof fn) {
fn(err, body); // XXX was resp
return;
}
console.info("");
if (err) {
console.warn("'" + service + "' may have failed." console.warn("'" + service + "' may have failed."
+ " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log"); + " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log");
console.warn(resp.statusCode, body); console.warn(err.statusCode, err.message);
//cb(new Error("not okay"), body); //cb(new Error("not okay"), body);
return; return;
} }
@ -413,6 +507,7 @@ var utils = {
body = JSON.parse(body); body = JSON.parse(body);
} catch(e) { } catch(e) {
// ignore // ignore
} }
if ("AWAIT_AUTH" === body.code) { if ("AWAIT_AUTH" === body.code) {
@ -439,62 +534,10 @@ var utils = {
} }
console.info(); console.info();
} }
}
var body = '';
if (resp.headers['content-length']) {
resp.on('data', function (chunk) {
body += chunk.toString();
});
resp.on('end', function () {
finish();
});
} else {
finish();
}
});
req.on('error', function (err) {
console.error('Put Config Error:');
console.error(err);
return;
});
req.end();
}
}; };
// Two styles:
// http 3000
// http modulename
function makeRpc(key) {
if (key !== argv[0]) {
return false;
}
utils.putConfig(argv[0], argv.slice(1));
return true;
} }
function packConfig(config) { function getToken(fn) {
return Object.keys(config).map(function (key) {
var val = config[key];
if ('undefined' === val) {
throw new Error("'undefined' used as a string value");
}
if ('undefined' === typeof val) {
//console.warn('[DEBUG]', key, 'is present but undefined');
return;
}
if (val && 'object' === typeof val && !Array.isArray(val)) {
val = JSON.stringify(val);
}
return key + ':' + val; // converts arrays to strings with ,
});
}
function getToken(err, state) {
if (err) {
console.error("Error while initializing config [init]:");
throw err;
}
state.relay = state.config.relay; state.relay = state.config.relay;
// { _otp, config: {} } // { _otp, config: {} }
@ -520,17 +563,7 @@ function getToken(err, state) {
state.config._otp = state.config._otp = authReq.otp; state.config._otp = state.config._otp = authReq.otp;
if (!state.config.token && state._can_pair) { if (!state.config.token && state._can_pair) {
console.info(""); console.info(TPLS.remote.code.replace(/0000/g, state.config._otp));
console.info("==============================================");
console.info(" Hey, Listen! ");
console.info("==============================================");
console.info(" ");
console.info(" GO CHECK YOUR EMAIL! ");
console.info(" ");
console.info(" DEVICE PAIR CODE: 0000 ".replace(/0000/g, state.config._otp));
console.info(" ");
console.info("==============================================");
console.info("");
} }
next(); next();
@ -541,7 +574,7 @@ function getToken(err, state) {
state._connecting = true; state._connecting = true;
// TODO use php-style object querification // TODO use php-style object querification
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) { RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) {
if (err) { if (err) {
state._error = err; state._error = err;
console.error("Error while initializing config [connect]:"); console.error("Error while initializing config [connect]:");
@ -550,7 +583,7 @@ function getToken(err, state) {
} }
console.info("waiting..."); console.info("waiting...");
next(); next();
}); }));
} }
, offer: function (token, next) { , offer: function (token, next) {
//console.log("[offer] Pairing Enabled by Relay"); //console.log("[offer] Pairing Enabled by Relay");
@ -565,7 +598,7 @@ function getToken(err, state) {
} catch(e) { } catch(e) {
console.warn("[warning] could not decode token"); console.warn("[warning] could not decode token");
} }
utils.putConfig('config', packConfig(state.config), function (err/*, body*/) { RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) {
if (err) { if (err) {
state._error = err; state._error = err;
console.error("Error while initializing config [offer]:"); console.error("Error while initializing config [offer]:");
@ -574,14 +607,14 @@ function getToken(err, state) {
} }
//console.log("Pairing Enabled Locally"); //console.log("Pairing Enabled Locally");
next(); next();
}); }));
} }
, granted: function (_, next) { , granted: function (_, next) {
//console.log("[grant] Pairing complete!"); //console.log("[grant] Pairing complete!");
next(); next();
} }
, end: function () { , end: function () {
utils.putConfig('enable', [], function (err) { RC.request({ service: 'enable', method: 'POST', data: [] }, handleRemoteRequest('enable', function (err) {
if (err) { console.error(err); return; } if (err) { console.error(err); return; }
console.info("Success"); console.info("Success");
@ -601,148 +634,14 @@ function getToken(err, state) {
} }
// end workaround // end workaround
parseCli(state); //parseCli(state);
}); fn();
}));
} }
}); });
} }
function parseCli(/*state*/) { RC.request({ service: 'config', method: 'GET' }, handleRemoteRequest('config', handleConfig));
var special = [
'false', 'none', 'off', 'disable'
, 'true', 'auto', 'on', 'enable'
];
if (-1 !== argv.indexOf('init')) {
utils.putConfig('list', []/*, function (err) {
}*/);
return;
}
if ([ 'ssh', 'http', 'tcp' ].some(function (key) {
if (key !== argv[0]) {
return false;
}
if (argv[1]) {
if (String(argv[1]) === String(parseInt(argv[1], 10))) {
// looks like a port
argv[1] = parseInt(argv[1], 10);
} else if (/\/|\\/.test(argv[1])) {
// looks like a path
argv[1] = path.resolve(argv[1]);
// TODO make a default assignment here
} else if (-1 === special.indexOf(argv[1])) {
console.error("Not sure what you meant by '" + argv[1] + "'.");
console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'");
return true;
}
utils.putConfig(argv[0], argv.slice(1));
return true;
}
return true;
})) {
return;
}
if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) {
return;
}
help();
process.exit(11);
}
function handleConfig(err, config) {
//console.log('CONFIG');
//console.log(config);
state.config = config;
var verstrd = [ pkg.name + ' daemon v' + state.config.version ];
if (state.config.version && state.config.version !== pkg.version) {
console.info(verstr.join(' '), verstrd.join(' '));
} else {
console.info(verstr.join(' '));
}
if (err) { console.error(err); process.exit(101); return; }
//
// check for init first, before anything else
// because it has arguments that may help in
// the next steps
//
if (-1 !== argv.indexOf('init')) {
parsers.init(argv, getToken);
return;
}
if (!state.config.relay || !state.config.token) {
if (!state.config.relay) {
state.config.relay = 'telebit.cloud';
}
//console.log("question the user?", Date.now());
askForConfig(state, function (err, state) {
// no errors actually get passed, so this is just future-proofing
if (err) { throw err; }
if (!state.config.token && state._can_pair) {
state.config._otp = common.otp();
}
//console.log("done questioning:", Date.now());
if (!state.token && !state.config.token) {
getToken(err, state);
} else {
parseCli(state);
}
});
return;
}
//console.log("no questioning:");
parseCli(state);
}
function parseConfig(err, text) {
try {
state._clientConfig = JSON.parse(text || '{}');
} catch(e1) {
try {
state._clientConfig = YAML.safeLoad(text || '{}');
} catch(e2) {
try {
state._clientConfig = TOML.parse(text || '');
} catch(e3) {
console.error(e1.message);
console.error(e2.message);
process.exit(1);
return;
}
}
}
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
common._init(
// make a default working dir and log dir
state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit')
, (state._clientConfig.root && path.join(state._clientConfig.root, 'etc'))
|| path.resolve(common.DEFAULT_CONFIG_PATH, '..')
);
state._ipc = common.pipename(state._clientConfig, true);
if (!Object.keys(state._clientConfig).length) {
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
console.info("");
}
if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) {
if (!err || 'ENOENT' === err.code) {
//console.warn("Empty config file. Run 'telebit init' to configure.\n");
} else {
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
}
}
utils.request({ service: 'config' }, handleConfig);
} }
var parsers = { var parsers = {

View File

@ -141,7 +141,7 @@ Use cases:
ssh = "Telebit SSH - The UNSTOPPABLE way to remote into your devices. ssh = "Telebit SSH - The UNSTOPPABLE way to remote into your devices.
usage: telebit ssh <auto|port> usage: telebit ssh <auto|port|none>
All https traffic will be inspected to see if it looks like ssh Once enabled all traffic that looks All https traffic will be inspected to see if it looks like ssh Once enabled all traffic that looks
@ -150,6 +150,7 @@ All https traffic will be inspected to see if it looks like ssh Once enabled all
ssh <port> forward ssh traffic to non-standard port ssh <port> forward ssh traffic to non-standard port
ex: telebit ssh 22 ex: explicitly forward ssh-looking packets to localhost:22 ex: telebit ssh 22 ex: explicitly forward ssh-looking packets to localhost:22
ssh none Disables ssh tunneling
Telebit SSH Client Telebit SSH Client
@ -451,5 +452,17 @@ The secret flags are:
[remote] [remote]
version = "telebit remote v{version}" version = "telebit remote v{version}"
code = "
==============================================
Hey, Listen!
==============================================
GO CHECK YOUR EMAIL!
DEVICE PAIR CODE: 0000
==============================================
"
[daemon] [daemon]
version = "telebit daemon v{version}" version = "telebit daemon v{version}"

View File

@ -0,0 +1,116 @@
'use strict';
var os = require('os');
var path = require('path');
var http = require('http');
var common = require('./cli-common.js');
function packConfig(config) {
return Object.keys(config).map(function (key) {
var val = config[key];
if ('undefined' === val) {
throw new Error("'undefined' used as a string value");
}
if ('undefined' === typeof val) {
//console.warn('[DEBUG]', key, 'is present but undefined');
return;
}
if (val && 'object' === typeof val && !Array.isArray(val)) {
val = JSON.stringify(val);
}
return key + ':' + val; // converts arrays to strings with ,
});
}
module.exports.create = function (state) {
common._init(
// make a default working dir and log dir
state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit')
, (state._clientConfig.root && path.join(state._clientConfig.root, 'etc'))
|| path.resolve(common.DEFAULT_CONFIG_PATH, '..')
);
state._ipc = common.pipename(state._clientConfig, true);
function makeResponder(service, resp, fn) {
var body = '';
function finish() {
var err;
if (200 !== resp.statusCode) {
err = new Error(body || ('get' + service + ' failed'));
err.statusCode = resp.statusCode;
err.code = "E_REQUEST";
}
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
fn(err, body);
}
if (!resp.headers['content-length'] && !resp.headers['content-type']) {
finish();
return;
}
// TODO use readable
resp.on('data', function (chunk) {
body += chunk.toString();
});
resp.on('end', finish);
}
var RC = {};
RC.request = function request(opts, fn) {
if (!opts) { opts = {}; }
var service = opts.service || 'config';
var args = opts.data;
if (args && 'control' === service) {
args = packConfig(args);
}
var json = JSON.stringify(args);
var url = '/rpc/' + service;
if (json) {
url += ('?_body=' + encodeURIComponent(json));
}
var method = opts.method || (args && 'POST') || 'GET';
var req = http.request({
socketPath: state._ipc.path
, method: method
, path: url
}, function (resp) {
makeResponder(service, resp, fn);
});
req.on('error', function (err) {
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
// ECONNREFUSED - leftover socket just needs to be restarted
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
if (opts._taketwo) {
fn(err);
return;
}
require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
if (err) { fn(err); return; }
opts._taketwo = true;
setTimeout(function () {
RC.request(opts, fn);
}, 2500);
});
return;
}
fn(err);
});
if ('POST' === method && opts.data) {
req.write(json || opts.data);
}
req.end();
};
return RC;
};