diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 4012747b..f96ac267 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -7,6 +7,7 @@ var key = require('../lib/key'); var init = require('../lib/init'); var logs = require('../lib/logs'); + var nameOption = { metavar: 'NAME', help: 'The name of the tessel on which the command will be executed' @@ -174,6 +175,34 @@ parser.command('list') }) .option('timeout', timeoutOption) .help('Lists all connected Tessels and their authorization status.'); +// accessing the root shell of your tessels +// Fixes issue https://github.com/tessel/t2-cli/issues/80 +/** +$ t2 root --help +> Usage: tessel root [-i ] [--help] +> -i : provide a path to the desired ssh key +$ + +$ t2 root +> Accessing root... +root@192.168.128.124 # +*/ +var functional_msg = '\nGain SSH root access to one of your authorized tessels (menu listing if multiple targets)'; +parser.command('root') + .usage(functional_msg + '\n\nUsage: t2 root [-i PATH] [--help]\n\n-i PATH: Optional targeting a different Private Key \n\n(Note: default target created by "t2 key generate" is ' + controller.TESSEL_AUTH_KEY + ')\n') + .option('path', { + abbr: 'i', + full: 'path', + metavar: 'PATH', + default: controller.TESSEL_AUTH_KEY, + help: 'Private Key (Note: created by "t2 key generate")' + }) + .callback(function(opts) { + controller.root(opts) + .then(closeSuccessfulCommand, closeFailedCommand); + }) + .help(functional_msg); + parser.command('init') .callback(init) diff --git a/lib/controller.js b/lib/controller.js index 784a4f19..39012398 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -7,7 +7,10 @@ var sprintf = require('sprintf-js').sprintf; var cp = require('child_process'); var async = require('async'); var updates = require('./update-fetch'); +var debug = require('debug')('controller'); +var Menu = require('terminal-menu'); var controller = {}; +controller.TESSEL_AUTH_KEY = Tessel.TESSEL_AUTH_PATH + '/id_rsa'; Tessel.list = function(opts) { @@ -92,7 +95,7 @@ Tessel.list = function(opts) { Tessel.get = function(opts) { return new Promise(function(resolve, reject) { - logs.info('Looking for your Tessel...'); + logs.info('Searching nearby LAN (netmask) and USB for Tessels...'); // Store the amount of time to look for Tessel in seconds var timeout = (opts.timeout || 2) * 1000; // Collection variable as more Tessels are found @@ -118,9 +121,22 @@ Tessel.get = function(opts) { controller.reconcileTessels(tessels) .then(function(reconciledTessels) { tessels = reconciledTessels; + // Run the heuristics to pick which Tessel to use return controller.runHeuristics(opts, tessels) - .then(logAndFinish); + .then(function(tessel){ + if(!tessel){ + controller.menu.prepareMenu(opts, tessels, resolve, reject,'Please select the default Tessel 2', function(name,index,resolve,reject){ + if(!name && name === 'EXIT'){ + reject('No Tessel selected!'); + } + logs.info('Selected Tessel: '+name); + logAndFinish(tessels[index]); + }); + } else { + logAndFinish(tessel); + } + }); }); } } @@ -150,15 +166,22 @@ Tessel.get = function(opts) { function logAndFinish(tessel) { // The Tessels that we won't be using should have their connections closed var connectionsToClose = tessels; - if (tessel) { - logs.info(sprintf('Connected to %s over %s', tessel.name, tessel.connection.connectionType)); - connectionsToClose.splice(tessels.indexOf(tessel), 1); + if(opts.root){ + + controller.closeTesselConnections(connectionsToClose) + .then(function() { + controller.ssh.runSSH(0, opts, tessels, resolve, reject); + }); + } else { + logs.info(sprintf('Connected to %s over %s', tessel.name, tessel.connection.connectionType)); + connectionsToClose.splice(tessels.indexOf(tessel), 1); - controller.closeTesselConnections(connectionsToClose) - .then(function() { - return resolve(tessel); - }); + controller.closeTesselConnections(connectionsToClose) + .then(function() { + return resolve(tessel); + }); + } } else { logs.info('Please specify a Tessel by name [--name ]'); controller.closeTesselConnections(connectionsToClose) @@ -180,9 +203,10 @@ may or may not be open and closes them controller.closeTesselConnections = function(tessels) { return new Promise(function(resolve, reject) { async.each(tessels, function closeThem(tessel, done) { + console.log('error: ',tessel); // If not an unauthorized LAN Tessel, it's connected - if (!(tessel.connection.connectionType === 'LAN' && - !tessel.connection.authorized)) { + if (!(tessel.lanConnection && + !tessel.lanConnection.authorized)) { // Close the connection return tessel.close() .then(done, done); @@ -333,7 +357,6 @@ controller.runHeuristics = function(opts, tessels) { } } } - // At this point, we know that no name option or env variable was set // and we know that there is only one USB and/or on LAN Tessel // We'll return the highest priority available @@ -641,6 +664,213 @@ controller.tesselFirmwareVerion = function(opts) { }); }); }; +controller.menu = { + + prepareMenu: function(opts, tessels, resolve, reject, title, callback) { + //return new Promise(function(resolve, reject) { + + var rtm = new Menu({ + // width: process.stdout.columns - 4, + width: 55, + x: 1, + y: 2, + bg: 'red' + }); + rtm.reset(); + rtm.write(title + '\n'); + rtm.write(' \n'); + + // create menu entries + for (var i in tessels) { + controller.menu.makeMenu(tessels[i], i, rtm); + } + + rtm.add('EXIT\n'); + rtm._draw(); + if (!opts.menu) { + // if this is no test, starting the menu as child process + controller.menu.showMenu(rtm, resolve, reject); + } else { + // because the test needs to resolve the promise ... + resolve({ + opts: opts, + tessels: tessels + }); + } + + rtm.once('select', function(label, index) { + rtm.close(); + controller.menu.clear(); + + // Identify the Exit command by first letter (all other entries start with numbers/index) + if (label[0] !== 'E') { + callback(tessels[index].name, index, resolve, reject); + // controller.ssh.runSSH(index, opts, tessels, resolve, reject); + } else { + // going to clear screen and calling exit to resolve promise + controller.menu.clear(); + controller.menu.exit(resolve); + } + }); + // }); + }, + makeMenu: function(tessel, index, rtm) { + if (tessel.lanConnection) { + if (!tessel.lanConnection.authorized) { + rtm.add(index + ') ' + tessel.name + ': ' + tessel.lanConnection.ip + ' (not authorized) \n'); + } else { + rtm.add(index + ') ' + tessel.name + ': ' + tessel.lanConnection.ip + ' (authorized) \n'); + } + + } else if (tessel.usbConnection) { + if (!tessel.usbConnection.authorized) { + rtm.add(index + ') ' + tessel.name + ': [USB] (not authorized) \n'); + } else { + rtm.add(index + ') ' + tessel.name + ': [USB] (authorized) \n'); + } + } + // FIXME: Workaround for trouble with dynamic added menu elements + rtm._draw(); + }, + showMenu: function(rtm, resolve, reject) { + // Menu navigation + process.stdin.pipe(rtm.createStream()).pipe(process.stdout); + + // raw mode for gain ability to navigate up and down within the menu + process.stdin.setRawMode(true); + + // cb + rtm.on('close', function() { + process.stdin.setRawMode(false); + process.stdin.end(); + }); + rtm.on('error', function(e) { + process.stdin.setRawMode(false); + reject(e); + }); + }, + seek: function(opts) { + // to be able to replace the seeker in testing mode + // return Tessel.seekTessels(opts); + return Tessel.get(opts); + }, + clear: function() { + // selected exit! + process.stdout.write('\u001b[2J\u001b[0;0H'); + }, + exit: function(resolve) { + resolve(); + //process.exit(); + } +}; +controller.ssh = { + runSSH: function(id, opts, tessels, resolve, reject) { + // clear console + debug('runSSH...\n root@' + tessels[id].lanConnection.host); + if (opts.menu) { + // because the test needs to resolve the promise ... + resolve({ + opts: opts, + tessels: tessels + }); + } else { + // if this is no test, starting the menu as child process + controller.menu.clear(); + var ch = require('child_process') + .spawn('ssh', ['-i', + opts.path, + 'root@' + tessels[id].lanConnection.ip + ], { + stdio: 'inherit' + }); + logs.info('Connect to ' + tessels[id].lanConnection.host + '...'); + + // FIXME: There is no handler for poweroff Tessel ! Needs to be fixed within the firmware... + // (Terminal freezes imediatelly after poweroff the Tessel) + + ch.once('error', function(e) { + if (e === 255) { + controller.menu.clear(); + controller.ssh.notAuthorized(); + reject(); + } else { + logs.warn('Error while connected to ' + tessels[id].lanConnection.host + ':', e); + reject(e); + } + + }); + ch.once('exit', function(e) { + + if (e === 255) { + controller.menu.clear(); + controller.ssh.notAuthorized(); + reject(); + } else if (e === 127) { // exit by user + // clear console + controller.menu.clear(); + logs.warn('Connection to ' + tessels[id].lanConnection.host + ' closed!\n'); + resolve(); + } else if (e === 0) { + // everything works fine ... now lets clear the terminal + controller.menu.clear(); + logs.info('Connection to ' + tessels[id].lanConnection.host + ' closed!\n'); + resolve(); + } else { + logs.warn('Connection to ' + tessels[id].lanConnection.host + ' closed due to reason:\n', e); + reject(e); + } + }); + + ch.once('close', function(e) { + // tessel powers off + if (e === 0) { + // everything works fine... + resolve(); + } else if (e === 255) { + //reject(); + process.exit(); + } else if (e === 127) { + reject(); + } else { + logs.warn('Connection to ' + tessels[id].lanConnection.host + ' closed due to reason:\n', e); + reject(); + } + }); + } + }, + notAuthorized: function() { + logs.warn('Sorry, you are not authorized!'); + logs.info('Maybe this Tessel didn\'t got "t2 provision" until now?'); + } +}; + +/* + The T2 root command is used to login into the firmware and gaining superuser access. + The security is provided by RSA - your Tessel get the keys while provisioning via USB. + + If you have only one Tessel, the T2 root command will login directly else there is + a ncurses like menu you can select the Tessel you like to gain root access... + + The structure of code and maybe unusual parts are paid due being testable. + The lightweight terminal-menu isn't written testable what needs a little bit hacking. + + Finally some parts are causing from my personnal learning curve about writing unit tests using sinon! +*/ +controller.root = function(opts) { + // ~ conversion to home because spawn isn't able to handle this right + if (opts.path && opts.path.substring(0, 1) === '~') { + var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; + if (/^~/.test(opts.path)) { + opts.path = opts.path.replace(/~/, home); + } + } + opts.root = true; + return new Promise(function(resolve, reject) { + controller.menu.seek(opts) + .then(resolve) + .catch(reject); + }); +}; module.exports = controller; diff --git a/lib/usb_connection.js b/lib/usb_connection.js index 5febd3a9..747d9c68 100644 --- a/lib/usb_connection.js +++ b/lib/usb_connection.js @@ -318,10 +318,12 @@ util.inherits(USB.Scanner, EventEmitter); USB.Scanner.prototype.start = function() { var self = this; - - usb.getDeviceList().forEach(deviceInspector); - - usb.on('attach', deviceInspector); + if (haveusb) { + usb.getDeviceList().forEach(deviceInspector); + usb.on('attach', deviceInspector); + } else { + logs.warn('Skipping USB discovering! (No USB controller)'); + } function deviceInspector(device) { if ((device.deviceDescriptor.idVendor === TESSEL_VID) && (device.deviceDescriptor.idProduct === TESSEL_PID)) { diff --git a/package.json b/package.json index 4bcb2a2a..8d17ea97 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "stream-to-buffer": "^0.1.0", "tar": "^2.1.1", "tar-stream": "^1.2.1", + "terminal-menu": "^2.1.1", "url-join": "0.0.1", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1"