alinex-ssh
Version:
Remote execution and tunneling through ssh.
742 lines (686 loc) • 21.6 kB
JavaScript
/*
SSH Connection class - API Usage
=================================================
This is an object oriented implementation arround the core `process.spawn`
command and alternatively ssh connections.
*/
(function() {
var async, chalk, config, connections, debug, debugData, debugDebug, debugTunnel, exec, findPort, forward, fs, groupResolve, init, net, open, optimize, path, portfinder, proxy, resolveServer, schema, setup, snip, ssh, util, validator, vitalStore;
debug = require('debug')('ssh');
debugTunnel = require('debug')('ssh:tunnel');
debugData = require('debug')('ssh:data');
debugDebug = require('debug')('ssh:debug');
chalk = require('chalk');
async = require('async');
net = require('net');
ssh = require('ssh2');
portfinder = require('portfinder');
exec = require('child_process').exec;
path = require('path');
fs = require('fs');
util = require('alinex-util');
config = require('alinex-config');
validator = null;
schema = require('./configSchema');
/*
Setup
-------------------------------------------------
*/
/*
Set the modules config paths and validation schema.
@param {Function(Error)} cb callback with `Error` if something went wrong
*/
exports.setup = setup = util["function"].once(this, function(cb) {
return config.setSchema('/ssh', schema, cb);
});
/*
Set the modules config paths, validation schema and initialize the configuration
@param {Function(Error)} cb callback with `Error` if something went wrong
*/
exports.init = init = util["function"].once(this, function(cb) {
debug("initialize");
return setup(function(err) {
if (err) {
return cb(err);
}
return config.init(cb);
});
});
connections = {};
vitalStore = {};
/*
Open Remote Connections
-------------------------------------------------
*/
/*
Open anew connection to run remote commands.
To close it again you have to call `conn.close()` but keep
in mind that this will close the connection for all runing commands and tunnels
because they are shared. So better only close it if you know you no longer need
it or at the end of your script using `ssh.close()`.
@param {Object} setup the server settings
- `server` {@schema configSchema.coffee#keys/server/entries/0}
- `group` {@schema configSchema.coffee#keys/group/entries/0}
- `retry` {@schema configSchema.coffee#keys/retry}
@param {Function(Error, Connection)} cb callback with error if something went wrong
and the ssh connection on success containing
*/
exports.connect = function(setup, cb) {
var error;
try {
setup = resolveServer(setup);
} catch (error1) {
error = error1;
return cb(error);
}
if (debug.enabled) {
if (validator == null) {
validator = require('alinex-validator');
}
try {
if (setup.server) {
validator.checkSync({
name: 'sshServerSetup',
title: "SSH Connection to Open",
value: setup.server,
schema: schema.keys.group.entries[0].entries
});
}
if (setup.group) {
validator.checkSync({
name: 'sshGroupSetup',
title: "SSH Group to Connect",
value: setup.group,
schema: schema.keys.group.entries[0]
});
}
if (setup.retry) {
validator.checkSync({
name: 'sshRetrySetup',
title: "SSH Retry Settings",
value: setup.retry,
schema: schema.keys.retry
});
}
} catch (error1) {
error = error1;
debug("called with " + util.inspect(setup, {
depth: null
}));
throw error;
}
}
return init(function(err) {
if (err) {
return cb(err);
}
return groupResolve(setup, function(err, setup) {
if (err) {
return cb(err);
}
return optimize(setup, function(err, setup) {
var ref, ref1, ref2, ref3, ref4, ref5, retry;
if (err) {
return cb(err);
}
retry = config.get("/ssh/retry");
return async.retry({
times: (ref = (ref1 = (ref2 = setup.retry) != null ? ref2.times : void 0) != null ? ref1 : retry) != null ? ref : 1,
interval: (ref3 = (ref4 = (ref5 = setup.retry) != null ? ref5.interval : void 0) != null ? ref4 : retry) != null ? ref3 : 200
}, function(cb) {
var problems;
problems = [];
return async.mapSeries(setup.server, function(entry, cb) {
return open(entry, function(err, conn) {
if (err) {
problems.push(err.message);
}
if (!conn) {
return cb();
}
return cb('STOP', conn);
});
}, function(_, result) {
var conn;
conn = result.pop();
if (!conn) {
return cb(new Error("Connecting to server impossible!\n" + problems.join("\n")));
}
conn.tunnel = {};
conn.process = {};
return cb(null, conn);
});
}, cb);
});
});
});
};
resolveServer = function(setup) {
var group, server;
if (typeof setup === 'string') {
if (group = config.get("/ssh/group/" + setup)) {
setup = {
group: group
};
} else if (server = config.get("/ssh/server/" + setup)) {
setup = {
server: server
};
} else {
throw new Error("Could not find group or server in ssh configuration with name '" + setup + "'");
}
}
if (typeof setup.server === 'object' && !Array.isArray(setup.server)) {
setup.server = [setup.server];
}
if (typeof setup.group === 'string') {
if (!(group = config.get("/ssh/group/" + setup.group))) {
throw new Error("Could not find group in ssh configuration with name '" + setup.group + "'");
}
setup.group = group;
}
return setup;
};
groupResolve = function(setup, cb) {
var check, now;
if (!setup.group) {
return cb(null, setup);
}
if (debug.enabled) {
debug(chalk.grey("check group for best value..."));
}
now = new Date().getTime();
check = now - 60000;
return async.map(setup.group, function(server, cb) {
var error, name, ref;
try {
server = resolveServer(server).server;
} catch (error1) {
error = error1;
return cb(error);
}
name = server[0].host;
if (((ref = vitalStore[name]) != null ? ref.date : void 0) > check) {
exports.connect;
return cb(null, {
server: server,
free: vitalStore[name].free
});
}
return exports.connect({
server: server,
retry: {
times: 0
}
}, function(err, conn) {
if (err) {
if (debug.enabled) {
debug(chalk.magenta(err.message));
}
vitalStore[name] = {
date: now,
free: -100
};
return cb(null, {
server: server,
free: -10
});
}
return conn.exec('nproc && cat /proc/loadavg', function(err, stream) {
var buffer;
buffer = "";
if (err) {
if (debug.enabled) {
debug(chalk.magenta(err.message));
}
vitalStore[name] = {
date: now,
free: -10
};
return cb(null, {
server: server,
free: -10
});
}
stream.on('data', function(data) {
return buffer += data.toString();
});
return stream.on('end', function() {
var data, free;
data = buffer.split(/\s+/);
free = data[0] - data[1];
if (debug.enabled) {
debug(chalk.grey(conn.name + ": vital data free: " + free));
}
vitalStore[name] = {
date: now,
free: free
};
return cb(null, {
server: server,
free: free,
conn: conn
});
});
});
});
}, function(err, result) {
var entry, i, len, ref;
if (err) {
return cb(err);
}
result = util.array.sortBy(result, '-free');
if (debug.enabled) {
debug(result[0].server[0].host + ": selected from cluster/group");
}
ref = result.slice(1);
for (i = 0, len = ref.length; i < len; i++) {
entry = ref[i];
if (entry.conn == null) {
continue;
}
entry.conn.done();
}
return cb(null, {
server: result[0].server,
retry: setup.retry
});
});
};
/*
Control tunnel creation
-------------------------------------------------
*/
/*
Open new tunnel. It may be closed by calling `tunnel.close()`. This will close
the tunnel but keeps the connection opened.
@param {Object} setup the server settings
- `server` {@schema configSchema.coffee#keys/server/entries/0}
- `tunnel` {@schema configSchema.coffee#keys/tunnel/entries/0}
But the `remote` server may be missing then the given `server` setting is used.
If the `host` and `port` setting is not given a socks5 proxy tunnel will be opened.
- `retry` {@schema configSchema.coffee#keys/retry}
@param {Function(Error, Object)} cb callback with error if something went wrong
or the tunnel information on success
*/
exports.tunnel = function(setup, cb) {
var conf, ref;
if (typeof setup === 'string') {
if (conf = config.get("/ssh/tunnel/" + setup)) {
setup = {
tunnel: conf
};
} else if (conf = config.get("/ssh/group/" + setup)) {
setup = {
group: conf
};
} else if (conf = config.get("/ssh/server/" + setup)) {
setup = {
server: conf
};
} else {
return cb(new Error("Could not find tunnel, group or server in ssh configuration with name '" + setup + "'"));
}
}
if ((setup.tunnel != null) && typeof setup.tunnel === 'string') {
if (conf = config.get("/ssh/tunnel/" + setup)) {
setup.tunnel = conf;
} else {
return cb(new Error("Could not find tunnel in ssh configuration with name '" + setup + "'"));
}
}
if ((ref = setup.tunnel) != null ? ref.remote : void 0) {
if (conf = config.get("/ssh/group/" + setup.tunnel.remote)) {
setup.group = conf;
} else if (conf = config.get("/ssh/server/" + setup.tunnel.remote)) {
setup.server = conf;
} else {
return cb(new Error("Could not find group or server in ssh configuration with name '" + setup.tunnel.remote + "'"));
}
}
return exports.connect(setup, function(err, conn) {
var base, ref1, ref2;
if (setup.tunnel == null) {
setup.tunnel = {};
}
if ((base = setup.tunnel).remote == null) {
base.remote = setup.server;
}
if (debugTunnel.enabled) {
if (validator == null) {
validator = require('alinex-validator');
}
validator.checkSync({
name: 'sshTunnelSetup',
title: "SSH Tunnel to Open",
value: setup.tunnel,
schema: schema.keys.tunnel.entries[0]
});
}
if (err) {
return cb(err);
}
if (((ref1 = setup.tunnel) != null ? ref1.host : void 0) && ((ref2 = setup.tunnel) != null ? ref2.port : void 0)) {
return forward(conn, setup.tunnel, function(err, tunnel) {
if (err) {
return cb(err);
}
return cb(null, tunnel);
});
} else {
return proxy(conn, setup.tunnel, function(err, tunnel) {
if (err) {
return cb(err);
}
return cb(null, tunnel);
});
}
});
};
/*
Close all tunnels and ssh connections.
This will end all operations and should be called on shutdown.
*/
exports.close = function() {
var conn, i, len, results;
results = [];
for (i = 0, len = connections.length; i < len; i++) {
conn = connections[i];
results.push(conn.close());
}
return results;
};
optimize = function(setup, cb) {
var server;
if (typeof setup === 'string') {
setup.server = config.get("/ssh/server/" + setup);
}
if (typeof setup.server === 'string') {
server = config.get("/ssh/server/" + setup.server);
if (!server) {
return cb(new Error("No server configured under /ssh/server/" + setup.server));
}
setup.server = server;
}
if (!Array.isArray(setup.server)) {
setup.server = [setup.server];
}
return async.each(setup.server, function(entry, cb) {
return async.parallel([
function(cb) {
var ref;
if (entry.username) {
return cb();
}
if (process.env.USERPROFILE) {
entry.username = process.env.USERPROFILE.split(path.sep)[2];
return cb();
}
entry.username = (ref = process.env.USER) != null ? ref : process.env.USERNAME;
if (entry.username) {
return cb();
}
return exec('whoami', {
encoding: 'utf8'
}, function(err, name) {
entry.username = name != null ? name.trim() : void 0;
return cb(err);
});
}, function(cb) {
var dir, home;
if (entry.password || entry.privateKey) {
return cb();
}
home = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
dir = process.env[home] + "/.ssh";
return fs.readdir(dir, function(err, files) {
if (err || !files.length) {
return cb();
}
return async.each(files, function(file, cb) {
return fs.readFile(dir + "/" + file, 'utf8', function(err, content) {
if (err) {
return cb();
}
if (!content.match(/-----BEGIN .*? PRIVATE KEY-----/)) {
return cb();
}
setup.server.push(util.extend(util.clone(entry), {
privateKey: content
}));
return cb();
});
}, cb);
});
}
], cb);
}, function(err) {
return cb(err, setup);
});
};
open = util["function"].onceTime(function(setup, cb) {
var conn, name, ref, ref1;
name = setup.host;
if ((ref = connections[name]) != null ? (ref1 = ref._sock) != null ? ref1._handle : void 0 : void 0) {
if (debug.enabled) {
debug(name + ": use existing connection");
}
return cb(null, connections[name]);
}
if (debug.enabled) {
debug(chalk.grey(name + ": establish new ssh connection"));
if (debugDebug.enabled) {
debugDebug(chalk.grey(util.inspect(setup).replace(/\n/g, '')));
}
}
conn = new ssh.Client();
conn.name = name;
conn.done = function() {
if (!Object.keys(conn.tunnel).length) {
if (!Object.keys(conn.process).length) {
return conn.close();
}
}
};
conn.close = function() {
var ref2;
delete connections[conn.name];
if ((ref2 = conn._sock) != null ? ref2._handle : void 0) {
return;
}
conn.end();
return conn.emit('end');
};
conn.on('ready', function() {
if (debug.enabled) {
debug(chalk.grey(conn.name + ": ssh client ready"));
}
connections[name] = conn;
return cb(null, conn);
});
if (debug.enabled) {
conn.on('banner', function(msg) {
return debug(chalk.yellow(msg));
});
}
conn.on('error', function(err) {
if (debug.enabled) {
debug(chalk.magenta(conn.name + ": got error: " + err.message));
}
conn.close();
return cb(err);
});
conn.on('end', function() {
var tunnel;
if (debug.enabled) {
debug(chalk.grey(conn.name + ": ssh client closed"));
}
for (tunnel in conn.tunnel) {
if (typeof tunnel.end === "function") {
tunnel.end();
}
}
return delete connections[name];
});
return conn.connect(util.extend(util.clone(setup), {
debug: !setup.debug ? null : function(msg) {
if (debugDebug.enabled) {
return debugDebug(chalk.grey(msg.replace(/DEBUG/, conn.name)));
}
}
}));
});
snip = function(data) {
var text;
text = util.inspect(data.toString());
if (text.length > 30) {
text = text.slice(0, 31) + '...\'';
}
return text;
};
forward = function(conn, setup, cb) {
var name;
name = setup.host + ":" + setup.port;
if (conn.tunnel[name]) {
return cb(null, conn.tunnel[name]);
}
if (debugTunnel.enabled) {
debugTunnel(conn.name + ": open new tunnel to " + name);
}
return findPort(setup, function(err, setup) {
var tunnel;
if (err) {
return cb(err);
}
if (setup.localHost == null) {
setup.localHost = '127.0.0.1';
}
if (debugTunnel.enabled) {
debugTunnel(chalk.grey(conn.name + ": opening tunnel on local port " + setup.localHost + ":" + setup.localPort));
}
tunnel = net.createServer(function(sock) {
return conn.forwardOut(sock.remoteAddress, sock.remotePort, setup.host, setup.port, function(err, stream) {
if (err) {
return tunnel.end();
}
sock.pipe(stream);
stream.pipe(sock);
if (debugData.enabled) {
sock.on('data', function(data) {
return debugData(chalk.grey("request : " + (snip(data))));
});
return stream.on('data', function(data) {
return debugData(chalk.grey("response: " + (snip(data))));
});
}
});
});
tunnel.end = function() {
try {
return tunnel.close();
} catch (error1) {}
};
tunnel.on('close', function() {
if (debugTunnel.enabled) {
debugTunnel(conn.name + ": closing tunnel to " + name);
}
delete conn.tunnel[name];
return conn.done();
});
tunnel.setup = {
host: setup.localHost,
port: setup.localPort
};
conn.tunnel[name] = tunnel;
return tunnel.listen(setup.localPort, setup.localHost, function() {
return cb(null, tunnel);
});
});
};
proxy = function(conn, setup, cb) {
var name, socks;
if (setup == null) {
setup = {};
}
socks = require('socksv5');
name = "socksv5 proxy";
if (conn.tunnel[name]) {
return cb(null, conn.tunnel[name]);
}
if (debugTunnel.enabled) {
debugTunnel(conn.name + ": open new tunnel to " + name);
}
return findPort(setup, function(err, setup) {
var tunnel;
if (err) {
return cb(err);
}
if (setup.localHost == null) {
setup.localHost = '127.0.0.1';
}
if (debugTunnel.enabled) {
debugTunnel(chalk.grey(conn.name + ": opening tunnel on local port " + setup.localHost + ":" + setup.localPort));
}
tunnel = socks.createServer(function(info, accept) {
return conn.forwardOut(info.srcAddr, info.srcPort, info.dstAddr, info.dstPort, function(err, stream) {
var sock;
if (err) {
return tunnel.end();
}
if (sock = accept(true)) {
sock.pipe(stream);
stream.pipe(sock);
if (debugData.enabled) {
sock.on('data', function(data) {
return debugData(chalk.grey("request : " + (snip(data))));
});
return stream.on('data', function(data) {
return debugData(chalk.grey("response: " + (snip(data))));
});
}
} else {
return tunnel.end();
}
});
});
tunnel.end = function() {
try {
return tunnel.close();
} catch (error1) {}
};
tunnel.on('close', function() {
if (debugTunnel.enabled) {
debugTunnel(conn.name + ": closing tunnel to " + name);
}
delete conn.tunnel[name];
if (!Object.keys(conn.tunnel).length) {
return conn.close();
}
});
tunnel.setup = {
host: setup.localHost,
port: setup.localPort
};
conn.tunnel[name] = tunnel;
tunnel.useAuth(socks.auth.None());
return tunnel.listen(setup.localPort, setup.localHost, function() {
return cb(null, tunnel);
});
});
};
findPort = function(setup, cb) {
var ref;
portfinder.basePort = (ref = setup.localPort) != null ? ref : 8000;
return portfinder.getPort(function(err, port) {
if (err) {
return cb(err);
}
if (debug.enabled && (setup.localPort != null) && port !== setup.localPort) {
debug(chalk.magenta("given port " + setup.localPort + " is blocked using " + port));
}
setup.localPort = port;
return cb(null, setup);
});
};
}).call(this);
//# sourceMappingURL=index.map