// native imports import { promisify } from 'util' import readline from 'readline' import { dirname } from 'path' import { readFile } from 'fs' const read = promisify(readFile) // third party imports import to from 'await-to-js' import { readFile as readConfig } from 'fs-read-data' import { Client } from 'ssh2' import logger from '@uci-utils/logger' import Conf from 'conf' let log = {} // declare module wide log to be set during construction // ssh client is already an event emitter class Ssh extends Client { constructor(opts={}) { super() this.opts = opts log = logger({id: opts.id }) this.ready = false this.config = new Conf({projectName:'ssh-uci'}) this.configsDir = process.env.SSH_CONFIG_DIR || this.config.get('configsDir') || dirname(this.config.path) log.debug({configsDir:this.configsDir, msg:'configuration files directory'}) this.rpath = opts.rpath || '/opt' // can be used to make all commands run in a particular remote path } async configure(opts,overwrite) { let configPath if (typeof opts ==='boolean' || !opts ) { if (typeof opts ==='boolean') overwrite = opts configPath = this.configsDir+'/'+this.config.get('defaultConfig') opts = await readConfig(configPath) } if (typeof opts === 'string') { configPath = (opts.indexOf('/') === -1)? this.configsDir+'/'+opts : opts opts = await readConfig(configPath) } log.debug({configPath:this.configPath, msg:'path to actual configuration file used'}) log.debug({opts:opts, msg:'connection options as read in from file'}) this.opts = overwrite ? Object.assign(this.opts,opts) : Object.assign(opts, this.opts) log.debug({opts:opts, msg:'connection options as ammended from configure() argument'}) } async connect(copts = {}) { const opts = Object.assign({},this.opts,copts) // merge any changes pasted via connect log.debug({opts:opts, msg:'final connection options as ammend via connect() argument and as used'}) if (opts.agentenv) opts.agent = process.env[opts.agentenv] opts.readyTimeout = opts.readyTimeout | 5000 // default was 20 seconds...too long opts.privateKey = opts.privateKey || opts.privateKeyPath ? await read(opts.privateKeyPath) : null // log.info(opts.privateKeyPath, opts.privateKey) super.connect(opts) return new Promise( (resolve,reject) => { this.on('ready', async () => { this.ready=true log.info(`connected to ${this.opts.host}`) resolve(`connected to ${this.opts.host}`) }) this.on('error', (err) => { log.warn({err:err, opts:this.opts, msg:'connection error'}) reject(err) }) }) } close() { this.ready=false log.info('closing ssh connection') this.end() } async shell(command, opts={}) { if (Object.prototype.toString.call(command) === '[object Object]') { opts = command if (!opts.cli) { this.close() let err = new Error('no terminal interactive shell must pass a command') log.fatal({command:command, opts:opts, err:err, msg:'no terminal interactive shell must pass a command'}) return err } command = [] // a place to store all terminal commands } opts.shell=true return this.exec(command,opts) } async exec(cmd = 'ls -la', opts={}) { if (!this.ready) { log.info('not connected, attempting connect first') let [err] = await to(this.connect()) if (err) return err } const remote = this // TODO bind this on promise instead const superexec = opts.shell? super.shell.bind(this) : super.exec.bind(this) let command = opts.shell ? opts : cmd // log.info(`executing command ${command} on shell? ${opts.shell}`) return new Promise(function(resolve, reject) { let reply ='' let error = '' superexec(command, function(err, stream) { log.info({command:command, cmd:cmd, opts:opts, msg:'before ssh exec/shell'}) const done = () => { if (error) remote.emit('remote:error',error) resolve({command:command,cmds:cmd,reply:reply,error:error}) } if (err) reject(err) stream.once('close', () => { if (remote.opts.close || opts.close || opts.cli) { remote.on('end', done) remote.close() } else done() }).on('data', function(data) { reply += data.toString() if (opts.cli) process.stdout.write(data.toString()) else log.info(`${remote.opts.host}$ ${data}`) }).stderr.on('data', function(data) { error += data.toString() if (opts.cli) process.stderr.write(data.toString()) else log.info(`${remote.opts.host}:ERR $: ${data}`) }) // alternate shell processing if (opts.shell) { if (opts.cli) { // terminal interactive shell sesssion const cli= readline.createInterface({input:process.stdin, output:process.stdout}) cli.on('line', line => { cmd.push(line) if(line.trim() === 'exit') { cli.removeAllListeners() cli.close() stream.end(`${line}\n`) } else { stream.write(`${line}\n`) } }) // end terminal listener } else { // give a bit of time for MOTD to display so it's not part of reply setTimeout(() => { reply='' stream.end(`${cmd}\nexit\n`) }, opts.delay || 300) } } // end shell processing }) // end super }) // end Promise } } export default Ssh