v1.4.0: add support for rsync and pass all leftover flags to rsync and/or ssh

This commit is contained in:
AJ ONeal 2018-09-11 23:46:25 -06:00
parent 8b641db470
commit d095381a40
4 changed files with 229 additions and 134 deletions

View File

@ -111,7 +111,13 @@ sclient telebit.cloud:443 - </path/to/file
### ssh over https ### ssh over https
```bash ```bash
sclient --ssh user@telebit.cloud sclient ssh user@telebit.cloud
```
### rsync over https
```bash
sclient rsync -av user@telebit.cloud:my-project/ ~/my-project/
``` ```
### Piping ### Piping

View File

@ -6,9 +6,6 @@ var local;
var isPiped = !process.stdin.isTTY; var isPiped = !process.stdin.isTTY;
var localAddress; var localAddress;
var localPort; var localPort;
var rejectUnauthorized;
var servername;
var sshProxy;
function usage() { function usage() {
console.info(""); console.info("");
@ -20,127 +17,220 @@ function usage() {
console.info(""); console.info("");
} }
function parseFlags() { function parseFlags(argv) {
process.argv.some(function (arg, i) { var args = argv.slice();
var flags = {};
args.sort(function (a, b) {
if ('-' === a[0]) {
if ('-' === b[0]) {
return 0;
}
return 1;
}
if ('-' === b[0]) {
return -1;
}
return 0;
});
args.some(function (arg, i) {
if (/^-k|--?insecure$/.test(arg)) { if (/^-k|--?insecure$/.test(arg)) {
rejectUnauthorized = false; flags.rejectUnauthorized = false;
process.argv.splice(i, 1); args.splice(i, 1);
return true; return true;
} }
}); });
process.argv.some(function (arg, i) { args.some(function (arg, i) {
if (/^--?servername$/.test(arg)) { if (/^--?servername$/.test(arg)) {
servername = process.argv[i + 1]; flags.servername = args[i + 1];
if (!servername || /^-/.test(servername)) { if (!flags.servername || /^-/.test(flags.servername)) {
usage(); usage();
process.exit(2); process.exit(2);
} }
process.argv.splice(i, 2); args.splice(i, 2);
return true; return true;
} }
}); });
process.argv.some(function (arg, i) { args.some(function (arg, i) {
if (/^--?p(ort)?$/.test(arg)) {
flags.port = args[i + 1];
if (!flags.port || /^-/.test(flags.port)) {
usage();
process.exit(201);
}
args.splice(i, 2);
return true;
}
});
args.some(function (arg, i) {
if (/^--?ssh$/.test(arg)) { if (/^--?ssh$/.test(arg)) {
sshProxy = true; flags.wrapSsh = true;
process.argv.splice(i, 1); args.splice(i, 1);
process.argv[3] = '$';
return true; return true;
} }
}); });
return {
flags: flags
, args: args
};
} }
parseFlags(); var sclient = require('../');
remote = (process.argv[2]||'').split(':'); function testRemote(opts) {
local = (process.argv[3]||'').split(':'); var emitter = new (require('events').EventEmitter)();
if ('ssh' === remote[0]) { sclient._test(opts).then(function () {
sshProxy = true; // connected successfully (and closed)
remote = local; sclient._listen(emitter, opts);
local = ['$']; }).catch(function (err) {
} else if ('ssh' === local[0]) { // did not connect succesfully
sshProxy = true; sclient._listen(emitter, opts);
local[0] = '$'; console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort
+ "' may not be accepting connections: ", err.toString(), '\n');
});
emitter.once('listening', function (opts) {
console.info('[listening] ' + opts.remoteAddr + ":" + opts.remotePort
+ " <= " + opts.localAddress + ":" + opts.localPort);
if (opts.command) {
var args = [
opts.remoteUser + 'localhost'
, '-p', opts.localPort
// we're _inverse_ proxying ssh, so we must alias the serveranem and ignore the IP
, '-o', 'HostKeyAlias=' + opts.remoteAddr
, '-o', 'CheckHostIP=no'
];
var spawn = require('child_process').spawn;
if ('rsync' === opts.command) {
var remote = args.shift() + ':' + opts.remotePath;
args = [ remote, '-e', 'ssh ' + args.join(' ') ];
}
args = args.concat(opts.args);
var child = spawn(opts.command, args, { stdio: 'inherit' });
child.on('exit', function () {
console.info('closing...');
});
child.on('close', function () {
opts.server.close();
});
}
});
emitter.on('connect', function (sock) {
console.info('[connect] ' + sock.localAddress.replace('::1', 'localhost') + ":" + sock.localPort
+ " => " + opts.remoteAddr + ":" + opts.remotePort);
});
emitter.on('remote-error', function (err) {
console.error('[error] (remote) ' + err.toString());
});
emitter.on('local-error', function (err) {
console.error('[error] (local) ' + err.toString());
});
} }
// arg 0 is node function main() {
// arg 1 is sclient var cmd = parseFlags(process.argv);
// arg 2 is remote var binParam;
// arg 3 is local var remoteUser;
if (4 !== process.argv.length) {
// Re-arrange argument order for ssh
if (cmd.flags.wrapSsh) {
cmd.args.splice(3, 0, 'ssh');
} else if (-1 !== [ 'ssh', 'rsync' ].indexOf((cmd.args[2]||'').split(':')[0])) {
cmd.flags.wrapSsh = true;
binParam = cmd.args.splice(2, 1);
cmd.args.splice(3, 0, binParam[0]);
}
remoteUser = (cmd.args[2]||'').split('@');
if (remoteUser[1]) {
// has 'user@' in front
remote = (remoteUser[1]||'').split(':');
remoteUser = remoteUser[0] + '@';
} else {
// no 'user@' in front
remote = (remoteUser[0]||'').split(':');
remoteUser = '';
}
local = (cmd.args[3]||'').split(':');
if (-1 !== [ 'ssh', 'rsync' ].indexOf(local[0])) {
cmd.flags.wrapSsh = true;
}
if (cmd.flags.wrapSsh) {
process.argv = cmd.args;
} else if (4 !== cmd.args.length) {
// arg 0 is node
// arg 1 is sclient
// arg 2 is remote
// arg 3 is local (or ssh or rsync)
if (isPiped) { if (isPiped) {
local = ['|']; local = ['|'];
} else { } else {
usage(); usage();
process.exit(1); process.exit(1);
} }
} }
// Check for the first argument (what to connect to) // Check for the first argument (what to connect to)
if (!remote[0]) { if (!remote[0]) {
usage(); usage();
return; return;
} }
if (!local[0]) { if (!local[0]) {
usage(); usage();
return; return;
} }
if (local[0] === String(parseInt(local[0], 10))) {
// check if it looks like a port number
if (local[0] === String(parseInt(local[0], 10))) {
localPort = parseInt(local[0], 10); localPort = parseInt(local[0], 10);
localAddress = 'localhost'; localAddress = 'localhost';
} else { } else {
localAddress = local[0]; // potentially '-' or '|' or '$' localAddress = local[0]; // potentially '-' or '|' or '$'
localPort = parseInt(local[1], 10); localPort = parseInt(local[1], 10);
} }
var rparts = remote[0].split('@'); var opts = {
var username = rparts[1] ? (rparts[0] + '@') : ''; remoteUser: remoteUser
var opts = { , remoteAddr: remote[0]
remoteAddr: rparts[1] || rparts[0] , remotePort: remote[1] || 443
, remotePort: remote[1] || 443 , localAddress: localAddress
, localAddress: localAddress , localPort: localPort
, localPort: localPort , rejectUnauthorized: cmd.flags.rejectUnauthorized
, rejectUnauthorized: rejectUnauthorized , servername: cmd.flags.servername
, servername: servername , stdin: null
, stdin: null , stdout: null
, stdout: null };
};
if ('-' === localAddress || '|' === localAddress) { if ('-' === localAddress || '|' === localAddress) {
opts.stdin = process.stdin; opts.stdin = process.stdin;
opts.stdout = process.stdout; opts.stdout = process.stdout;
// no need for port // no need for port
} else if ('$' === localAddress) { } else if (-1 !== [ 'ssh', 'rsync' ].indexOf(localAddress)) {
sshProxy = true; cmd.flags.wrapSsh = true;
opts.localAddress = 'localhost'; opts.localAddress = 'localhost';
opts.localPort = 0; // choose at random opts.localPort = local[1] || 0; // choose at random
} else if (!localPort) { opts.command = localAddress;
opts.args = cmd.args.slice(4); // node, sclient, ssh, addr
if ('rsync' === opts.command) {
opts.remotePath = opts.remotePort;
opts.remotePort = 0;
}
if (!opts.remotePort) {
opts.remotePort = cmd.flags.port || 443;
}
} else if (!localPort) {
usage(); usage();
return; return;
}
testRemote(opts);
} }
var emitter = require('../')(opts); main();
emitter.once('listening', function (opts) {
var port = opts.localPort;
console.info('[listening] ' + opts.remoteAddr + ":" + opts.remotePort
+ " <= " + opts.localAddress + ":" + opts.localPort);
if (sshProxy) {
// TODO choose at random and connect to ssh after test
var spawn = require('child_process').spawn;
var ssh = spawn('ssh', [
username + 'localhost'
, '-p', port
// we're _inverse_ proxying ssh, so we must alias the serveranem and ignore the IP
, '-o', 'HostKeyAlias=' + opts.remoteAddr
, '-o', 'CheckHostIP=no'
], { stdio: 'inherit' });
ssh.on('exit', function () {
console.info('shutting down...');
});
ssh.on('close', function () {
opts.server.close();
});
}
});

View File

@ -1,5 +1,7 @@
'use strict'; 'use strict';
var PromiseA = global.Promise;
var net = require('net'); var net = require('net');
var tls = require('tls'); var tls = require('tls');
@ -9,20 +11,19 @@ function listenForConns(emitter, opts) {
servername: opts.remoteAddr, host: opts.remoteAddr, port: opts.remotePort servername: opts.remoteAddr, host: opts.remoteAddr, port: opts.remotePort
, rejectUnauthorized: opts.rejectUnauthorized , rejectUnauthorized: opts.rejectUnauthorized
}, function () { }, function () {
console.info('[connect] ' + sclient.localAddress.replace('::1', 'localhost') + ":" + sclient.localPort emitter.emit('connect', sclient);
+ " => " + opts.remoteAddr + ":" + opts.remotePort);
c.pipe(sclient); c.pipe(sclient);
sclient.pipe(out || c); sclient.pipe(out || c);
}); });
sclient.on('error', function (err) { sclient.on('error', function (err) {
console.error('[error] (remote) ' + err.toString()); emitter.emit('remote-error', err);
}); });
c.on('error', function (err) { c.on('error', function (err) {
console.error('[error] (local) ' + err.toString()); emitter.emit('local-error', err);
}); });
if (out) { if (out) {
out.on('error', function (err) { out.on('error', function (err) {
console.error('[error] (local) ' + err.toString()); emitter.emit('local-error', err);
}); });
} }
} }
@ -47,8 +48,7 @@ function listenForConns(emitter, opts) {
} }
function testConn(opts) { function testConn(opts) {
var emitter = new (require('events').EventEmitter)(); return new PromiseA(function (resolve, reject) {
// Test connection first // Test connection first
var tlsOpts = { var tlsOpts = {
host: opts.remoteAddr, port: opts.remotePort host: opts.remoteAddr, port: opts.remotePort
@ -63,17 +63,16 @@ function testConn(opts) {
tlsOpts.ALPNProtocols = [ 'http', 'h2' ]; tlsOpts.ALPNProtocols = [ 'http', 'h2' ];
} }
var tlsSock = tls.connect(tlsOpts, function () { var tlsSock = tls.connect(tlsOpts, function () {
setTimeout(function () {
tlsSock.end(); tlsSock.end();
listenForConns(emitter, opts); resolve();
}, 200);
}); });
tlsSock.on('error', function (err) { tlsSock.on('error', function (err) {
console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n'); reject(err);
listenForConns(emitter, opts); });
}); });
return emitter;
} }
module.exports = testConn; // no public exports yet
// the API is for the commandline only
module.exports._test = testConn;
module.exports._listen = listenForConns;

View File

@ -1,6 +1,6 @@
{ {
"name": "sclient", "name": "sclient",
"version": "1.3.0", "version": "1.4.0",
"description": "Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally. Also ideal for multiplexing a single port with multiple protocols using SNI.", "description": "Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally. Also ideal for multiplexing a single port with multiple protocols using SNI.",
"main": "index.js", "main": "index.js",
"homepage": "https://telebit.cloud/sclient/", "homepage": "https://telebit.cloud/sclient/",