diff --git a/bin/tty.js b/bin/tty.js index 8b4e1441..b5387baf 100755 --- a/bin/tty.js +++ b/bin/tty.js @@ -9,7 +9,7 @@ process.title = 'tty.js'; var tty = require('../'); -var conf = tty.config.readConfig() +var conf = tty.config.readConfig(['/etc/tty.js.d/', process.env.HOME+'/.config/tty.js', process.env.TTYJS_CONFD]) , app = tty.createServer(conf); app.listen(); diff --git a/lib/config.js b/lib/config.js index cdc1bc35..db56fdfa 100644 --- a/lib/config.js +++ b/lib/config.js @@ -5,7 +5,8 @@ var path = require('path') , fs = require('fs') - , logger = require('./logger'); + , logger = require('./logger') + , vm = require('vm'); /** * Options @@ -14,43 +15,108 @@ var path = require('path') var options; /** - * Read Config + * Read configuration files from given directories. + * + * For each directory, read all configuration files with .json extension or + * execute all configuration scripts with .js extension and merge results + * into single configuration object at top level. If file passed instead of + * directory, just read it as .json file. + * + * JSON configuration example: + *
{ shell: '/bin/bash' }
+ * + * Configuration script example: + *
 shell = function(tty, options) { return [ '/bin/bash' ]; };
*/ - -function readConfig(file) { +function readConfig(dirs) { var home = process.env.HOME , conf = {} , dir , json; - if (file || options.config) { - file = path.resolve(process.cwd(), file || options.config); - dir = path.dirname(file); - json = options.config; - } else { - dir = process.env.TTYJS_PATH || path.join(home, '.tty.js'); - json = path.join(dir, 'config.json'); + // Convert argument to array, for clarity + if(typeof(dirs) === 'string') { + dirs = [ dirs ]; + } else if (typeof(dirs)==='undefined') { + dirs = []; } - if (exists(dir) && exists(json)) { - if (!fs.statSync(dir).isDirectory()) { - json = dir; - dir = home; + dirs.forEach(function(dir) { + //*DEBUG*/logger.log("Configuration directory: \""+dir+"\"."); + + // Skip undefined values numbers and objects + if(typeof(dir) === 'undefined') { return; } + if(typeof(dir) !== 'string') { + logger.error("Unexepected argument: expected string (path to configuration directory or file), got: \""+typeof(dir)+"\". Argument value: \""+dir+"\"."); + return; } - conf = JSON.parse(fs.readFileSync(json, 'utf8')); - } else { - if (!exists(dir)) { - fs.mkdirSync(dir, 0700); + // Skip non-directories + if(!fs.existsSync(dir)) { + //*DEBUG*/logger.log("Configuration directory \""+dir+"\" is not exists, skipping."); + return; } - fs.writeFileSync(json, JSON.stringify(conf, null, 2)); - fs.chmodSync(json, 0600); - } + // If file given, then read it directly + var dirStats = fs.statSync(dir); + if(dirStats.isFile()) { + try { + // Read file and merge it with previous settings + logger.log("Reading file \""+file+"\" from directory \""+dir+"\"."); + merge(conf, JSON.parse(fs.readFileSync(file, 'utf8'))); + } catch(e) { + logger.error("Cannot read configuration file \""+file+"\": ", e.stack); + } + + return; + } + + if(!dirStats.isDirectory()) { + logger.error("Expected a directory to read configuration files from: \""+dir+"\". Check file system."); + return; + } - // expose paths - conf.dir = dir; - conf.json = json; + //*DEBUG*/logger.log("Reading configuration files from directory \""+dir+"\"."); + try { + fs.readdirSync(dir).sort().forEach(function(file) { + if(typeof(file)!=='string') { return; } + file = path.join(dir, file); + + var result = {}; + + // If file has .json extension + if(endsWith(file, ".json")) { + try { + // Read file, parse it, and merge it with previous settings + //*DEBUG*/logger.log("Reading configuration file \""+file+"\"."); + merge(conf, JSON.parse(fs.readFileSync(file, 'utf8'))); + } catch(e) { + logger.error("Cannot read configuration file \""+file+"\": ",e.stack); + return; + } + } else if(endsWith(file, ".js")) { + try { + //*DEBUG*/logger.log("Executing configuration script \""+file+"\"."); + + // Put logger into scope to allow functions to produce log messages + conf.logger = logger; + + // Read file and execute it in scope of previous configuration to merge with. + vm.runInNewContext(fs.readFileSync(file, 'utf8'), conf, file); + } catch(e) { + logger.error("Cannot execute configuration script \""+file+"\": ", e.stack); + return; + } + } else { + //*DEBUG*/logger.log("File \""+file+"\" has incorrect extension (not .json, nor .js). Skipping."); + return; + } + + }); + } catch(e) { + logger.error("Cannot read configuration files from directory \""+dir+"\": ", e.stack); + } + }); // flag conf.__read = true; @@ -58,6 +124,10 @@ function readConfig(file) { return checkConfig(conf); } +function endsWith(string, suffix) { + return string.indexOf(suffix, string.length - suffix.length) !== -1; +}; + function checkConfig(conf) { if (typeof conf === 'string') { return readConfig(conf); @@ -102,13 +172,7 @@ function checkConfig(conf) { conf.hostname; // '0.0.0.0' // shell, process name - if (conf.shell && ~conf.shell.indexOf('/')) { - conf.shell = path.resolve(conf.dir, conf.shell); - } - conf.shell = conf.shell || process.env.SHELL || 'sh'; - - // arguments to shell, if they exist - conf.shellArgs = conf.shellArgs || []; + conf.shell = conf.shell || process.env.SHELL || '/bin/sh'; // static directory conf.static = tryResolve(conf.dir, conf.static || 'static'); diff --git a/lib/tty.js b/lib/tty.js index 78849eb8..844be4f5 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -185,8 +185,8 @@ Server.prototype.handleConnection = function(socket) { // XXX Possibly wrap socket events from inside Session // constructor, and do: session.on('create') // or session.on('create term'). - socket.on('create', function(cols, rows, func) { - return session.handleCreate(cols, rows, func); + socket.on('create', function(cols, rows, options, func) { + return session.handleCreate(cols, rows, options, func); }); socket.on('data', function(id, data) { @@ -421,12 +421,115 @@ Session.prototype.sync = function() { this.socket.emit('sync', terms); }; -Session.prototype.handleCreate = function(cols, rows, func) { + +/* + * Generate shell command using configuration and options. + * + * Configuration options are: + * + * + * User supplied options are: + * + */ +Session.prototype.prepareShellCommand = function Session_prepareShellCommand(conf, options) { + + // For backward compatibility: get shell command and arguments from configuration + var shell = typeof conf.shell === 'function' + ? conf.shell(this, options) + : conf.shell; + + if(typeof(shell) === 'string') { + shell = [ shell ]; + } + + options = options || {}; + + // Chose shell using options + if(options.protocol) { + try { + switch(options.protocol) { + // "local" means local user + case 'local': + shell = conf.localUserShell? conf.localUserShell(options) : [ 'su', '--login', options.user || 'root' , + '--shell=' + shell[0] ].concat( (shell.length>1)? [ '--command='+shell.slice(1).join(' ') ] : [] ); + break; + + // ssh means access a remote host via ssh. See also "remoteHost" option. + // + // If host at remote IP will be reinstalled, then it key will be different, + // so ssh will complain about that. + case 'ssh': + shell = conf.sshShell? conf.sshShell(options) : [ 'ssh', '-t', ((options.user)? options.user : 'root') + '@' + options.host ].concat(shell); + break; + + // docker means access to a docker container at a host. If tty.js + // itself will be ran in a container, then "remoteHost" option will be + // required for this protocol to work. + case 'docker': + shell = conf.dockerShell? conf.dockerShell(options) : [ 'docker', 'exec', '-it', options.container ].concat(shell); + break; + + default: + // For unknown protocol, try to call protocol handler from configuration with name "${protocol}Shell". + shell = conf[options.protocol+"Shell"](); + break; + } + } catch(e) { + this.error("Cannot create shell for protocol: \""+options.protocol+"\". Options: ", options, ", error: ", e); + return "/bin/false"; // Safe default + } + } + + if(options.remoteHost) { + try { + // Execute shell at remote host using ssh with host key checking disabled. + // The reason for that is that user hosts can be reinstalled often using + shell = (conf.remoteHostCommand)? conf.remoteHostCommand(options.remoteHost, shell, options) : + [ 'ssh', '-t', '-q', '-o', 'StrictHostKeyChecking no', '-o', 'UserKnownHostsFile=/dev/null', '-l', 'root', options.remoteHost ].concat(shell); + } catch(e) { + this.error("Cannot create command to execute shell on remote host: \""+options.remoteHost+"\". Options: ", options, ", error: ", e); + return "/bin/false"; // Safe default + } + } + + //*DEBUG*/this.log("conf.shell: ", conf.shell, ", options: ", options, ", shell: ", shell); + + return shell; +} + +// Options are for prepareShellCommand() function. +Session.prototype.handleCreate = function(cols, rows, options, func) { var self = this , terms = this.terms , conf = this.server.conf , socket = this.socket; + //*DEBUG*/this.log('Options: ', options); + var len = Object.keys(terms).length , term , id; @@ -436,15 +539,10 @@ Session.prototype.handleCreate = function(cols, rows, func) { return func({ error: 'Terminal limit.' }); } - var shell = typeof conf.shell === 'function' - ? conf.shell(this) - : conf.shell; - - var shellArgs = typeof conf.shellArgs === 'function' - ? conf.shellArgs(this) - : conf.shellArgs; + // Get path to shell binary from configuration + var shellCmd = this.prepareShellCommand(conf, options); - term = pty.fork(shell, shellArgs, { + term = pty.fork(shellCmd[0], shellCmd.slice(1), { name: conf.termName, cols: cols, rows: rows, @@ -478,7 +576,7 @@ Session.prototype.handleCreate = function(cols, rows, func) { return func(null, { id: id, pty: term.pty, - process: sanitize(conf.shell) + process: sanitize(shellCmd[0]) }); }; diff --git a/static/index.html b/static/index.html index 458d295e..1a4c8b15 100644 --- a/static/index.html +++ b/static/index.html @@ -1,4 +1,4 @@ - + tty.js @@ -13,11 +13,47 @@

tty.js

Click the tilde with a modifier to close the window.

- - + + diff --git a/static/tty.js b/static/tty.js index 45af0bff..12a762b9 100644 --- a/static/tty.js +++ b/static/tty.js @@ -12,10 +12,7 @@ var document = this.document , window = this , root - , body - , h1 - , open - , lights; + , body; /** * Initial Document Title @@ -52,15 +49,19 @@ tty.elements; * Open */ -tty.open = function() { - if (document.location.pathname) { - var parts = document.location.pathname.split('/') - , base = parts.slice(0, parts.length - 1).join('/') + '/' - , resource = base.substring(1) + 'socket.io'; - - tty.socket = io.connect(null, { resource: resource }); +tty.open = function(socket) { + if(socket) { + tty.socket = socket; } else { - tty.socket = io.connect(); + if (document.location.pathname) { + var parts = document.location.pathname.split('/') + , base = parts.slice(0, parts.length - 1).join('/') + '/' + , resource = base.substring(1) + 'socket.io'; + + tty.socket = io.connect(null, { resource: resource }); + } else { + tty.socket = io.connect(); + } } tty.windows = []; @@ -69,28 +70,10 @@ tty.open = function() { tty.elements = { root: document.documentElement, body: document.body, - h1: document.getElementsByTagName('h1')[0], - open: document.getElementById('open'), - lights: document.getElementById('lights') }; root = tty.elements.root; body = tty.elements.body; - h1 = tty.elements.h1; - open = tty.elements.open; - lights = tty.elements.lights; - - if (open) { - on(open, 'click', function() { - new Window; - }); - } - - if (lights) { - on(lights, 'click', function() { - tty.toggleLights(); - }); - } tty.socket.on('connect', function() { tty.reset(); @@ -119,7 +102,7 @@ tty.open = function() { Object.keys(terms).forEach(function(key) { var data = terms[key] - , win = new Window + , win = new Window() , tab = win.tabs[0]; delete tty.terms[tab.id]; @@ -182,21 +165,11 @@ tty.reset = function() { tty.emit('reset'); }; -/** - * Lights - */ - -tty.toggleLights = function() { - root.className = !root.className - ? 'dark' - : ''; -}; - /** * Window */ -function Window(socket) { +function Window(socket, options) { var self = this; EventEmitter.call(this); @@ -223,7 +196,7 @@ function Window(socket) { title = document.createElement('div'); title.className = 'title'; - title.innerHTML = ''; + title.innerHTML = options.title || ''; this.socket = socket || tty.socket; this.element = el; @@ -231,6 +204,7 @@ function Window(socket) { this.bar = bar; this.button = button; this.title = title; + this.options = options || {}; this.tabs = []; this.focused = null; @@ -511,7 +485,7 @@ Window.prototype.each = function(func) { }; Window.prototype.createTab = function() { - return new Tab(this, this.socket); + return new Tab(this, this.socket, this.options); }; Window.prototype.highlight = function() { @@ -552,8 +526,7 @@ Window.prototype.previousTab = function() { /** * Tab */ - -function Tab(win, socket) { +function Tab(win, socket, options) { var self = this; var cols = win.cols @@ -589,7 +562,7 @@ function Tab(win, socket) { win.tabs.push(this); - this.socket.emit('create', cols, rows, function(err, data) { + this.socket.emit('create', cols, rows, options, function(err, data) { if (err) return self._destroy(); self.pty = data.pty; self.id = data.id; @@ -618,7 +591,6 @@ Tab.prototype.handleTitle = function(title) { if (Terminal.focus === this) { document.title = title; - // if (h1) h1.innerHTML = title; } if (this.window.focused === this) { @@ -653,7 +625,7 @@ Tab.prototype.focus = function() { win.element.appendChild(this.element); win.focused = this; - win.title.innerHTML = this.process; + win.title.innerHTML = (this.options.title || 'local') + ' - ' + this.process; document.title = this.title || initialTitle; this.button.style.fontWeight = 'bold'; this.button.style.color = ''; @@ -702,11 +674,6 @@ Tab.prototype._destroy = function() { win.destroy(); } - // if (!tty.windows.length) { - // document.title = initialTitle; - // if (h1) h1.innerHTML = initialTitle; - // } - this.__destroy(); }; @@ -860,7 +827,7 @@ Tab.prototype.setProcessName = function(name) { // if (this.title) { // name += ' (' + this.title + ')'; // } - this.window.title.innerHTML = name; + this.window.title.innerHTML = (this.window.options.title || 'local') + ' - ' + name; } }; @@ -886,23 +853,6 @@ function sanitize(text) { return (text + '').replace(/[&<>]/g, '') } -/** - * Load - */ - -function load() { - if (load.done) return; - load.done = true; - - off(document, 'load', load); - off(document, 'DOMContentLoaded', load); - tty.open(); -} - -on(document, 'load', load); -on(document, 'DOMContentLoaded', load); -setTimeout(load, 200); - /** * Expose */