v1.4.0: add support for rsync and pass all leftover flags to rsync and/or ssh
This commit is contained in:
parent
8b641db470
commit
d095381a40
|
@ -111,7 +111,13 @@ sclient telebit.cloud:443 - </path/to/file
|
|||
### ssh over https
|
||||
|
||||
```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
|
||||
|
|
292
bin/sclient.js
292
bin/sclient.js
|
@ -6,9 +6,6 @@ var local;
|
|||
var isPiped = !process.stdin.isTTY;
|
||||
var localAddress;
|
||||
var localPort;
|
||||
var rejectUnauthorized;
|
||||
var servername;
|
||||
var sshProxy;
|
||||
|
||||
function usage() {
|
||||
console.info("");
|
||||
|
@ -20,127 +17,220 @@ function usage() {
|
|||
console.info("");
|
||||
}
|
||||
|
||||
function parseFlags() {
|
||||
process.argv.some(function (arg, i) {
|
||||
function parseFlags(argv) {
|
||||
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)) {
|
||||
rejectUnauthorized = false;
|
||||
process.argv.splice(i, 1);
|
||||
flags.rejectUnauthorized = false;
|
||||
args.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
process.argv.some(function (arg, i) {
|
||||
args.some(function (arg, i) {
|
||||
if (/^--?servername$/.test(arg)) {
|
||||
servername = process.argv[i + 1];
|
||||
if (!servername || /^-/.test(servername)) {
|
||||
flags.servername = args[i + 1];
|
||||
if (!flags.servername || /^-/.test(flags.servername)) {
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
process.argv.splice(i, 2);
|
||||
args.splice(i, 2);
|
||||
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)) {
|
||||
sshProxy = true;
|
||||
process.argv.splice(i, 1);
|
||||
process.argv[3] = '$';
|
||||
flags.wrapSsh = true;
|
||||
args.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
flags: flags
|
||||
, args: args
|
||||
};
|
||||
}
|
||||
|
||||
parseFlags();
|
||||
var sclient = require('../');
|
||||
|
||||
remote = (process.argv[2]||'').split(':');
|
||||
local = (process.argv[3]||'').split(':');
|
||||
function testRemote(opts) {
|
||||
var emitter = new (require('events').EventEmitter)();
|
||||
|
||||
if ('ssh' === remote[0]) {
|
||||
sshProxy = true;
|
||||
remote = local;
|
||||
local = ['$'];
|
||||
} else if ('ssh' === local[0]) {
|
||||
sshProxy = true;
|
||||
local[0] = '$';
|
||||
sclient._test(opts).then(function () {
|
||||
// connected successfully (and closed)
|
||||
sclient._listen(emitter, opts);
|
||||
}).catch(function (err) {
|
||||
// did not connect succesfully
|
||||
sclient._listen(emitter, opts);
|
||||
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
|
||||
// arg 1 is sclient
|
||||
// arg 2 is remote
|
||||
// arg 3 is local
|
||||
if (4 !== process.argv.length) {
|
||||
if (isPiped) {
|
||||
local = ['|'];
|
||||
function main() {
|
||||
var cmd = parseFlags(process.argv);
|
||||
var binParam;
|
||||
var remoteUser;
|
||||
|
||||
// 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) {
|
||||
local = ['|'];
|
||||
} else {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for the first argument (what to connect to)
|
||||
if (!remote[0]) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for the first argument (what to connect to)
|
||||
if (!remote[0]) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!local[0]) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
if (local[0] === String(parseInt(local[0], 10))) {
|
||||
localPort = parseInt(local[0], 10);
|
||||
localAddress = 'localhost';
|
||||
} else {
|
||||
localAddress = local[0]; // potentially '-' or '|' or '$'
|
||||
localPort = parseInt(local[1], 10);
|
||||
}
|
||||
|
||||
var rparts = remote[0].split('@');
|
||||
var username = rparts[1] ? (rparts[0] + '@') : '';
|
||||
var opts = {
|
||||
remoteAddr: rparts[1] || rparts[0]
|
||||
, remotePort: remote[1] || 443
|
||||
, localAddress: localAddress
|
||||
, localPort: localPort
|
||||
, rejectUnauthorized: rejectUnauthorized
|
||||
, servername: servername
|
||||
, stdin: null
|
||||
, stdout: null
|
||||
};
|
||||
|
||||
if ('-' === localAddress || '|' === localAddress) {
|
||||
opts.stdin = process.stdin;
|
||||
opts.stdout = process.stdout;
|
||||
// no need for port
|
||||
} else if ('$' === localAddress) {
|
||||
sshProxy = true;
|
||||
opts.localAddress = 'localhost';
|
||||
opts.localPort = 0; // choose at random
|
||||
} else if (!localPort) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
var emitter = require('../')(opts);
|
||||
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();
|
||||
});
|
||||
if (!local[0]) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// check if it looks like a port number
|
||||
if (local[0] === String(parseInt(local[0], 10))) {
|
||||
localPort = parseInt(local[0], 10);
|
||||
localAddress = 'localhost';
|
||||
} else {
|
||||
localAddress = local[0]; // potentially '-' or '|' or '$'
|
||||
localPort = parseInt(local[1], 10);
|
||||
}
|
||||
|
||||
var opts = {
|
||||
remoteUser: remoteUser
|
||||
, remoteAddr: remote[0]
|
||||
, remotePort: remote[1] || 443
|
||||
, localAddress: localAddress
|
||||
, localPort: localPort
|
||||
, rejectUnauthorized: cmd.flags.rejectUnauthorized
|
||||
, servername: cmd.flags.servername
|
||||
, stdin: null
|
||||
, stdout: null
|
||||
};
|
||||
|
||||
if ('-' === localAddress || '|' === localAddress) {
|
||||
opts.stdin = process.stdin;
|
||||
opts.stdout = process.stdout;
|
||||
// no need for port
|
||||
} else if (-1 !== [ 'ssh', 'rsync' ].indexOf(localAddress)) {
|
||||
cmd.flags.wrapSsh = true;
|
||||
opts.localAddress = 'localhost';
|
||||
opts.localPort = local[1] || 0; // choose at random
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
testRemote(opts);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
61
index.js
61
index.js
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA = global.Promise;
|
||||
|
||||
var net = require('net');
|
||||
var tls = require('tls');
|
||||
|
||||
|
@ -9,20 +11,19 @@ function listenForConns(emitter, opts) {
|
|||
servername: opts.remoteAddr, host: opts.remoteAddr, port: opts.remotePort
|
||||
, rejectUnauthorized: opts.rejectUnauthorized
|
||||
}, function () {
|
||||
console.info('[connect] ' + sclient.localAddress.replace('::1', 'localhost') + ":" + sclient.localPort
|
||||
+ " => " + opts.remoteAddr + ":" + opts.remotePort);
|
||||
emitter.emit('connect', sclient);
|
||||
c.pipe(sclient);
|
||||
sclient.pipe(out || c);
|
||||
});
|
||||
sclient.on('error', function (err) {
|
||||
console.error('[error] (remote) ' + err.toString());
|
||||
emitter.emit('remote-error', err);
|
||||
});
|
||||
c.on('error', function (err) {
|
||||
console.error('[error] (local) ' + err.toString());
|
||||
emitter.emit('local-error', err);
|
||||
});
|
||||
if (out) {
|
||||
out.on('error', function (err) {
|
||||
console.error('[error] (local) ' + err.toString());
|
||||
emitter.emit('local-error', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -47,33 +48,31 @@ function listenForConns(emitter, opts) {
|
|||
}
|
||||
|
||||
function testConn(opts) {
|
||||
var emitter = new (require('events').EventEmitter)();
|
||||
|
||||
// Test connection first
|
||||
var tlsOpts = {
|
||||
host: opts.remoteAddr, port: opts.remotePort
|
||||
, rejectUnauthorized: opts.rejectUnauthorized
|
||||
};
|
||||
if (opts.servername) {
|
||||
tlsOpts.servername = opts.servername;
|
||||
} else if (/^[\w\.\-]+\.[a-z]{2,}$/i.test(opts.remoteAddr)) {
|
||||
tlsOpts.servername = opts.remoteAddr.toLowerCase();
|
||||
}
|
||||
if (opts.alpn) {
|
||||
tlsOpts.ALPNProtocols = [ 'http', 'h2' ];
|
||||
}
|
||||
var tlsSock = tls.connect(tlsOpts, function () {
|
||||
setTimeout(function () {
|
||||
return new PromiseA(function (resolve, reject) {
|
||||
// Test connection first
|
||||
var tlsOpts = {
|
||||
host: opts.remoteAddr, port: opts.remotePort
|
||||
, rejectUnauthorized: opts.rejectUnauthorized
|
||||
};
|
||||
if (opts.servername) {
|
||||
tlsOpts.servername = opts.servername;
|
||||
} else if (/^[\w\.\-]+\.[a-z]{2,}$/i.test(opts.remoteAddr)) {
|
||||
tlsOpts.servername = opts.remoteAddr.toLowerCase();
|
||||
}
|
||||
if (opts.alpn) {
|
||||
tlsOpts.ALPNProtocols = [ 'http', 'h2' ];
|
||||
}
|
||||
var tlsSock = tls.connect(tlsOpts, function () {
|
||||
tlsSock.end();
|
||||
listenForConns(emitter, opts);
|
||||
}, 200);
|
||||
resolve();
|
||||
});
|
||||
tlsSock.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
tlsSock.on('error', function (err) {
|
||||
console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n');
|
||||
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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.",
|
||||
"main": "index.js",
|
||||
"homepage": "https://telebit.cloud/sclient/",
|
||||
|
|
Loading…
Reference in New Issue