Compare commits
	
		
			10 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6e796da80a | |||
| 415ed10b99 | |||
| 6fdf889b0b | |||
| c345d9ec69 | |||
| 58cbe914c1 | |||
| 7add115e5f | |||
| d390df175a | |||
| d095381a40 | |||
| 8b641db470 | |||
| 0361e5762d | 
							
								
								
									
										40
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								README.md
									
									
									
									
									
								
							@ -30,6 +30,24 @@ cURL
 | 
			
		||||
$ 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
 | 
			
		||||
@ -51,12 +69,12 @@ Usage
 | 
			
		||||
=====
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sclient [flags] <remote> <local>
 | 
			
		||||
sclient [flags] [ssh|rsync] <remote> [local]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* flags
 | 
			
		||||
  * -k, --insecure ignore invalid TLS (SSL/HTTPS) certificates
 | 
			
		||||
  * --servername <string> spoof SNI (to disable use IP as <remote> and do not use this option)
 | 
			
		||||
  * `-k, --insecure` ignore invalid TLS (SSL/HTTPS) certificates
 | 
			
		||||
  * `--servername <string>` spoof SNI (to disable use IP as <remote> and do not use this option)
 | 
			
		||||
* remote
 | 
			
		||||
  * must have servername (i.e. example.com)
 | 
			
		||||
  * 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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Reading from stdin
 | 
			
		||||
### Reading from stdin
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sclient telebit.cloud:443 -
 | 
			
		||||
@ -95,7 +113,19 @@ sclient telebit.cloud:443 -
 | 
			
		||||
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
 | 
			
		||||
printf "GET / HTTP/1.1\r\nHost: telebit.cloud\r\n\r\n" | sclient telebit.cloud:443
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										218
									
								
								bin/sclient.js
									
									
									
									
									
								
							
							
						
						
									
										218
									
								
								bin/sclient.js
									
									
									
									
									
								
							@ -1,3 +1,4 @@
 | 
			
		||||
#!/usr/bin/env node
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var pkg = require('../package.json');
 | 
			
		||||
@ -6,8 +7,6 @@ var local;
 | 
			
		||||
var isPiped = !process.stdin.isTTY;
 | 
			
		||||
var localAddress;
 | 
			
		||||
var localPort;
 | 
			
		||||
var rejectUnauthorized;
 | 
			
		||||
var servername;
 | 
			
		||||
 | 
			
		||||
function usage() {
 | 
			
		||||
  console.info("");
 | 
			
		||||
@ -19,37 +18,174 @@ function usage() {
 | 
			
		||||
  console.info("");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseFlags() {
 | 
			
		||||
  process.argv.some(function (arg, i) {
 | 
			
		||||
function parseFlags(argv) {
 | 
			
		||||
  var args = argv.slice();
 | 
			
		||||
  var flags = {};
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  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(':');
 | 
			
		||||
local = (process.argv[3]||'').split(':');
 | 
			
		||||
function testRemote(opts) {
 | 
			
		||||
  var emitter = new (require('events').EventEmitter)();
 | 
			
		||||
 | 
			
		||||
  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(' ') ];
 | 
			
		||||
      }
 | 
			
		||||
      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
 | 
			
		||||
if (4 !== process.argv.length) {
 | 
			
		||||
    // arg 3 is local (or ssh or rsync)
 | 
			
		||||
    if (isPiped) {
 | 
			
		||||
      local = ['|'];
 | 
			
		||||
    } else {
 | 
			
		||||
@ -68,27 +204,59 @@ 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 '|'
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
var opts = {
 | 
			
		||||
  remoteAddr: remote[0]
 | 
			
		||||
, remotePort: remote[1] || 443
 | 
			
		||||
, localAddress: localAddress
 | 
			
		||||
, localPort: localPort
 | 
			
		||||
, rejectUnauthorized: rejectUnauthorized
 | 
			
		||||
, servername: servername
 | 
			
		||||
};
 | 
			
		||||
require('../')(opts);
 | 
			
		||||
  testRemote(opts);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								index.js
									
									
									
									
									
								
							@ -1,34 +1,35 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var PromiseA = global.Promise;
 | 
			
		||||
 | 
			
		||||
var net = require('net');
 | 
			
		||||
var tls = require('tls');
 | 
			
		||||
 | 
			
		||||
function listenForConns(opts) {
 | 
			
		||||
function listenForConns(emitter, opts) {
 | 
			
		||||
  function pipeConn(c, out) {
 | 
			
		||||
    var sclient = tls.connect({
 | 
			
		||||
      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);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ('-' === opts.localAddress || '|' === opts.localAddress) {
 | 
			
		||||
    pipeConn(process.stdin, process.stdout);
 | 
			
		||||
    pipeConn(opts.stdin, opts.stdout);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -40,12 +41,14 @@ function listenForConns(opts) {
 | 
			
		||||
    host: opts.localAddress
 | 
			
		||||
  , port: opts.localPort
 | 
			
		||||
  }, function () {
 | 
			
		||||
    console.info('[listening] ' + opts.remoteAddr + ":" + opts.remotePort
 | 
			
		||||
      + " <= " + opts.localAddress + ":" + opts.localPort);
 | 
			
		||||
    opts.localPort = this.address().port;
 | 
			
		||||
    opts.server = this;
 | 
			
		||||
    emitter.emit('listening', opts);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function testConn(opts) {
 | 
			
		||||
  return new PromiseA(function (resolve, reject) {
 | 
			
		||||
    // Test connection first
 | 
			
		||||
    var tlsOpts = {
 | 
			
		||||
      host: opts.remoteAddr, port: opts.remotePort
 | 
			
		||||
@ -53,15 +56,23 @@ function testConn(opts) {
 | 
			
		||||
    };
 | 
			
		||||
    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(opts);
 | 
			
		||||
      resolve();
 | 
			
		||||
    });
 | 
			
		||||
    tlsSock.on('error', function (err) {
 | 
			
		||||
    console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n');
 | 
			
		||||
    listenForConns(opts);
 | 
			
		||||
      reject(err);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.2.2",
 | 
			
		||||
  "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.",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "homepage": "https://telebit.cloud/sclient/",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user