2
1
mirror of https://git.coolaj86.com/coolaj86/sclient.js synced 2025-04-21 21:50:37 +00:00

Compare commits

..

18 Commits

Author SHA1 Message Date
6e796da80a v1.4.3: add sh header to js bin file 2018-10-15 15:39:33 -06:00
415ed10b99 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/sclient.js 2018-09-23 01:16:19 -06:00
6fdf889b0b v1.4.2: add undocumented staging features: vpn and --socks5 <port> 2018-09-12 13:57:16 -06:00
c345d9ec69 v1.4.2: add undocumented staging features: vpn and --socks5 <port> 2018-09-12 02:10:20 -06:00
58cbe914c1 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/sclient.js 2018-09-12 01:48:12 -06:00
7add115e5f v1.4.1: update README 2018-09-12 00:29:38 -06:00
d390df175a v1.4.1: update README 2018-09-12 00:28:28 -06:00
d095381a40 v1.4.0: add support for rsync and pass all leftover flags to rsync and/or ssh 2018-09-11 23:46:25 -06:00
8b641db470 more ssh proxy examples 2018-09-11 01:58:49 -06:00
0361e5762d v1.3.0: add inverse ssh proxy 2018-09-11 01:51:42 -06:00
eba2b4e5b2 v1.2.2: bugfix use 443 as default remote port 2018-09-11 01:23:41 -06:00
a1a16005c1 v1.2.2: bugfix use 443 as default remote port 2018-09-10 23:39:34 -06:00
fa3de139a4 v1.2.1: update homepage url 2018-09-03 19:00:15 -06:00
1287ccc1ce v1.2.0: bring up to par with sclient.go; add --servername, allow pipes, use - for stdin 2018-09-03 15:29:19 -06:00
f14708495a v1.0.4: typo fix in docs and cURL example 2018-08-06 17:36:07 -06:00
a052db3a23 v1.0.3: fix typo in docs 2018-08-06 16:50:15 -06:00
5439f89482 v1.0.2: add homepage link and some keywords 2018-08-06 12:49:17 -06:00
8705e845cd update keywords 2018-08-06 12:33:15 -06:00
4 changed files with 368 additions and 66 deletions

View File

@ -5,11 +5,11 @@ Secure Client for exposing TLS (aka SSL) secured services as plain-text connecti
Also ideal for multiplexing a single port with multiple protocols using SNI. Also ideal for multiplexing a single port with multiple protocols using SNI.
Unwrap a TLS connection Unwrap a TLS connection:
```bash ```bash
$ sclient whatever.com:443 localhost:3000 $ sclient whatever.com:443 localhost:3000
> [listening] telebit.cloud:443 <= localhost:3000 > [listening] whatever.com:443 <= localhost:3000
``` ```
Connect via Telnet Connect via Telnet
@ -21,9 +21,35 @@ $ telnet localhost 3000
Connect via netcat (nc) Connect via netcat (nc)
```bash ```bash
$ nc telnet localhost 3000 $ nc localhost 3000
``` ```
cURL
```bash
$ curl http://localhost:3000 -H 'Host: whatever.com'
```
Inverse SSH proxy (ssh over https):
```bash
$ sclient ssh user@example.com
```
(this is the same as a normal SSH Proxy, just easier to type):
```bash
$ ssh -o ProxyCommand="sclient %h" user@example.com
```
Inverse rsync proxy (rsync over https):
```bash
$ sclient rsync user@example.com:path/ path/
```
A poor man's (or Windows user's) makeshift replacement for `openssl s_client`, `stunnel`, or `socat`.
Install Install
======= =======
@ -43,9 +69,12 @@ Usage
===== =====
```bash ```bash
sclient <remote> <local> [-k | --insecure] sclient [flags] [ssh|rsync] <remote> [local]
``` ```
* flags
* `-k, --insecure` ignore invalid TLS (SSL/HTTPS) certificates
* `--servername <string>` spoof SNI (to disable use IP as &lt;remote&gt; and do not use this option)
* remote * remote
* must have servername (i.e. example.com) * must have servername (i.e. example.com)
* port is optional (default is 443) * port is optional (default is 443)
@ -71,5 +100,43 @@ sclient telebit.cloud:443 localhost:3000
Ignore a bad TLS/SSL/HTTPS certificate and connect anyway. Ignore a bad TLS/SSL/HTTPS certificate and connect anyway.
```bash ```bash
sclient badtls.telebit.cloud:443 localhost:3000 -k sclient -k badtls.telebit.cloud:443 localhost:3000
```
### Reading from stdin
```bash
sclient telebit.cloud:443 -
```
```bash
sclient telebit.cloud:443 - </path/to/file
```
### ssh over https
```bash
sclient ssh user@telebit.cloud
```
### rsync over https
```bash
sclient rsync -av user@telebit.cloud:my-project/ ~/my-project/
```
### Piping
```bash
printf "GET / HTTP/1.1\r\nHost: telebit.cloud\r\n\r\n" | sclient telebit.cloud:443
```
Testing for security vulnerabilities on the remote:
```bash
sclient --servername "Robert'); DROP TABLE Students;" -k example.com localhost:3000
```
```bash
sclient --servername "../../../.hidden/private.txt" -k example.com localhost:3000
``` ```

View File

@ -1,57 +1,262 @@
#!/usr/bin/env node
'use strict'; 'use strict';
var pkg = require('../package.json'); var pkg = require('../package.json');
var remote = (process.argv[2]||'').split(':'); var remote;
var local = (process.argv[3]||'').split(':'); var local;
var isPiped = !process.stdin.isTTY;
var localAddress; var localAddress;
var localPort; var localPort;
var rejectUnauthorized;
function usage() { function usage() {
console.info(""); console.info("");
console.info("sclient.js v" + pkg.version); console.info("sclient.js v" + pkg.version);
console.info("Usage: sclient <servername:port> <port> [-k | --insecure]"); console.info("Usage: sclient [--servername <string>] [-k | --insecure] <servername:port> <port>");
console.info(" ex: sclient whatever.com 3000"); console.info(" ex: sclient whatever.com 3000");
console.info(" (whatever.com:443 localhost:3000)"); console.info(" (whatever.com:443 localhost:3000)");
console.info(" ex: sclient whatever.com:4080 0.0.0.0:3000"); console.info(" ex: sclient whatever.com:4080 0.0.0.0:3000");
console.info(""); console.info("");
process.exit(0);
} }
if (!remote[0]) { function parseFlags(argv) {
usage(); var args = argv.slice();
return; var flags = {};
}
if (!remote[1]) { args.some(function (arg, i) {
remote[1] = 443; if (/^-k|--?insecure$/.test(arg)) {
flags.rejectUnauthorized = false;
args.splice(i, 1);
return true;
}
});
args.some(function (arg, i) {
if (/^--?servername$/.test(arg)) {
flags.servername = args[i + 1];
if (!flags.servername || /^-/.test(flags.servername)) {
usage();
process.exit(2);
}
args.splice(i, 2);
return true;
}
});
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)) {
flags.wrapSsh = true;
args.splice(i, 1);
return true;
}
});
args.some(function (arg, i) {
if (/^--?socks5$/.test(arg)) {
flags.socks5 = args[i + 1];
if (!flags.socks5 || /^-/.test(flags.socks5)) {
usage();
process.exit(202);
}
args.splice(i, 2);
return true;
}
});
// This works for most (but not all)
// of the ssh and rsync flags - because they mostly don't have arguments
args.sort(function (a, b) {
if ('-' === a[0]) {
if ('-' === b[0]) {
return 0;
}
return 1;
}
if ('-' === b[0]) {
return -1;
}
return 0;
});
return {
flags: flags
, args: args
};
} }
if (!local[0]) { var sclient = require('../');
usage();
return; function testRemote(opts) {
} var emitter = new (require('events').EventEmitter)();
if (local[0] === String(parseInt(local[0], 10))) {
localPort = parseInt(local[0], 10); sclient._test(opts).then(function () {
localAddress = 'localhost'; // connected successfully (and closed)
} else { sclient._listen(emitter, opts);
localAddress = local[0]; }).catch(function (err) {
localPort = parseInt(local[1], 10); // 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(' ') ];
}
if (opts.socks5) {
args.push('-D');
args.push('localhost:' + opts.socks5);
}
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());
});
} }
if (!localPort) { function main() {
usage(); var cmd = parseFlags(process.argv);
return; 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', 'vpn' ].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', 'vpn' ].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();
return;
}
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', 'vpn' ].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
opts.socks5 = cmd.flags.socks5;
if ('rsync' === opts.command) {
opts.remotePath = opts.remotePort;
opts.remotePort = 0;
}
if ('vpn' === opts.command) {
opts.command = 'ssh';
if (!opts.socks5) {
usage();
return;
}
}
if (!opts.remotePort) {
opts.remotePort = cmd.flags.port || 443;
}
} else if (!localPort) {
usage();
return;
}
testRemote(opts);
} }
if (/^-k|--insecure$/.test(process.argv[4])) { main();
rejectUnauthorized = false;
}
var opts = {
remoteAddr: remote[0]
, remotePort: remote[1]
, localAddress: localAddress
, localPort: localPort
, rejectUnauthorized: rejectUnauthorized
};
require('../')(opts);

View File

@ -1,26 +1,39 @@
'use strict'; 'use strict';
var PromiseA = global.Promise;
var net = require('net'); var net = require('net');
var tls = require('tls'); var tls = require('tls');
function listenForConns(opts) { function listenForConns(emitter, opts) {
var server = net.createServer(function (c) { function pipeConn(c, out) {
var sclient = tls.connect({ var sclient = tls.connect({
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(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) {
out.on('error', function (err) {
emitter.emit('local-error', err);
});
}
}
if ('-' === opts.localAddress || '|' === opts.localAddress) {
pipeConn(opts.stdin, opts.stdout);
return;
}
var server = net.createServer(pipeConn);
server.on('error', function (err) { server.on('error', function (err) {
console.error('[error] ' + err.toString()); console.error('[error] ' + err.toString());
}); });
@ -28,24 +41,38 @@ function listenForConns(opts) {
host: opts.localAddress host: opts.localAddress
, port: opts.localPort , port: opts.localPort
}, function () { }, function () {
console.info('[listening] ' + opts.remoteAddr + ":" + opts.remotePort opts.localPort = this.address().port;
+ " <= " + opts.localAddress + ":" + opts.localPort); opts.server = this;
emitter.emit('listening', opts);
}); });
} }
function testConn(opts) { function testConn(opts) {
// Test connection first return new PromiseA(function (resolve, reject) {
var tlsSock = tls.connect({ // Test connection first
servername: opts.remoteAddr, host: opts.remoteAddr, port: opts.remotePort var tlsOpts = {
, rejectUnauthorized: opts.rejectUnauthorized host: opts.remoteAddr, port: opts.remotePort
}, function () { , rejectUnauthorized: opts.rejectUnauthorized
tlsSock.end(); };
listenForConns(opts); if (opts.servername) {
}); tlsOpts.servername = opts.servername;
tlsSock.on('error', function (err) { } else if (/^[\w\.\-]+\.[a-z]{2,}$/i.test(opts.remoteAddr)) {
console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n'); tlsOpts.servername = opts.remoteAddr.toLowerCase();
listenForConns(opts); }
if (opts.alpn) {
tlsOpts.ALPNProtocols = [ 'http', 'h2' ];
}
var tlsSock = tls.connect(tlsOpts, function () {
tlsSock.end();
resolve();
});
tlsSock.on('error', function (err) {
reject(err);
});
}); });
} }
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,8 +1,9 @@
{ {
"name": "sclient", "name": "sclient",
"version": "1.0.0", "version": "1.4.3",
"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/",
"bin": { "bin": {
"sclient": "bin/sclient.js" "sclient": "bin/sclient.js"
}, },
@ -19,9 +20,11 @@
}, },
"keywords": [ "keywords": [
"stunnel", "stunnel",
"s_client", "openssl s_client",
"telebit", "telebit",
"openssl" "netcat",
"telnet",
"socat"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)" "license": "(MIT OR Apache-2.0)"