mirror of
https://git.coolaj86.com/coolaj86/sclient.js
synced 2025-04-21 21:50:37 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e796da80a | |||
415ed10b99 | |||
6fdf889b0b | |||
c345d9ec69 | |||
58cbe914c1 | |||
7add115e5f | |||
d390df175a | |||
d095381a40 | |||
8b641db470 | |||
0361e5762d | |||
eba2b4e5b2 | |||
a1a16005c1 |
40
README.md
40
README.md
@ -30,6 +30,24 @@ cURL
|
|||||||
$ curl http://localhost:3000 -H 'Host: whatever.com'
|
$ 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`.
|
A poor man's (or Windows user's) makeshift replacement for `openssl s_client`, `stunnel`, or `socat`.
|
||||||
|
|
||||||
Install
|
Install
|
||||||
@ -51,12 +69,12 @@ Usage
|
|||||||
=====
|
=====
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sclient [flags] <remote> <local>
|
sclient [flags] [ssh|rsync] <remote> [local]
|
||||||
```
|
```
|
||||||
|
|
||||||
* flags
|
* flags
|
||||||
* -k, --insecure ignore invalid TLS (SSL/HTTPS) certificates
|
* `-k, --insecure` ignore invalid TLS (SSL/HTTPS) certificates
|
||||||
* --servername <string> spoof SNI (to disable use IP as <remote> and do not use this option)
|
* `--servername <string>` spoof SNI (to disable use IP as <remote> 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)
|
||||||
@ -85,7 +103,7 @@ Ignore a bad TLS/SSL/HTTPS certificate and connect anyway.
|
|||||||
sclient -k badtls.telebit.cloud:443 localhost:3000
|
sclient -k badtls.telebit.cloud:443 localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
Reading from stdin
|
### Reading from stdin
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sclient telebit.cloud:443 -
|
sclient telebit.cloud:443 -
|
||||||
@ -95,7 +113,19 @@ sclient telebit.cloud:443 -
|
|||||||
sclient telebit.cloud:443 - </path/to/file
|
sclient telebit.cloud:443 - </path/to/file
|
||||||
```
|
```
|
||||||
|
|
||||||
Piping
|
### 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
|
```bash
|
||||||
printf "GET / HTTP/1.1\r\nHost: telebit.cloud\r\n\r\n" | sclient telebit.cloud:443
|
printf "GET / HTTP/1.1\r\nHost: telebit.cloud\r\n\r\n" | sclient telebit.cloud:443
|
||||||
|
246
bin/sclient.js
246
bin/sclient.js
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var pkg = require('../package.json');
|
var pkg = require('../package.json');
|
||||||
@ -6,8 +7,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;
|
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
console.info("");
|
console.info("");
|
||||||
@ -19,76 +18,245 @@ function usage() {
|
|||||||
console.info("");
|
console.info("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFlags() {
|
function parseFlags(argv) {
|
||||||
process.argv.some(function (arg, i) {
|
var args = argv.slice();
|
||||||
|
var flags = {};
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
parseFlags();
|
var sclient = require('../');
|
||||||
|
|
||||||
remote = (process.argv[2]||'').split(':');
|
function testRemote(opts) {
|
||||||
local = (process.argv[3]||'').split(':');
|
var emitter = new (require('events').EventEmitter)();
|
||||||
|
|
||||||
// arg 0 is node
|
sclient._test(opts).then(function () {
|
||||||
// arg 1 is sclient
|
// connected successfully (and closed)
|
||||||
// arg 2 is remote
|
sclient._listen(emitter, opts);
|
||||||
// arg 3 is local
|
}).catch(function (err) {
|
||||||
if (4 !== process.argv.length) {
|
// 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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', '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) {
|
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 '|'
|
localAddress = local[0]; // potentially '-' or '|' or '$'
|
||||||
localPort = parseInt(local[1], 10);
|
localPort = parseInt(local[1], 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('-' === localAddress || '|' === localAddress) {
|
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
|
// no need for port
|
||||||
} else if (!localPort) {
|
} 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();
|
usage();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!opts.remotePort) {
|
||||||
|
opts.remotePort = cmd.flags.port || 443;
|
||||||
|
}
|
||||||
|
} else if (!localPort) {
|
||||||
|
usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
testRemote(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts = {
|
main();
|
||||||
remoteAddr: remote[0]
|
|
||||||
, remotePort: remote[1]
|
|
||||||
, localAddress: localAddress
|
|
||||||
, localPort: localPort
|
|
||||||
, rejectUnauthorized: rejectUnauthorized
|
|
||||||
, servername: servername
|
|
||||||
};
|
|
||||||
require('../')(opts);
|
|
||||||
|
37
index.js
37
index.js
@ -1,34 +1,35 @@
|
|||||||
'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) {
|
||||||
function pipeConn(c, out) {
|
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(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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('-' === opts.localAddress || '|' === opts.localAddress) {
|
if ('-' === opts.localAddress || '|' === opts.localAddress) {
|
||||||
pipeConn(process.stdin, process.stdout);
|
pipeConn(opts.stdin, opts.stdout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,12 +41,14 @@ 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) {
|
||||||
|
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
|
||||||
@ -53,15 +56,23 @@ function testConn(opts) {
|
|||||||
};
|
};
|
||||||
if (opts.servername) {
|
if (opts.servername) {
|
||||||
tlsOpts.servername = 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 () {
|
var tlsSock = tls.connect(tlsOpts, function () {
|
||||||
tlsSock.end();
|
tlsSock.end();
|
||||||
listenForConns(opts);
|
resolve();
|
||||||
});
|
});
|
||||||
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(opts);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
"name": "sclient",
|
||||||
"version": "1.2.1",
|
"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/",
|
"homepage": "https://telebit.cloud/sclient/",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user