diff --git a/README.md b/README.md index 49e2bb3..8f07bca 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,13 @@ sclient telebit.cloud:443 - " + 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(); diff --git a/index.js b/index.js index ea87a0c..2b3aa4b 100644 --- a/index.js +++ b/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; diff --git a/package.json b/package.json index e381859..37459a4 100644 --- a/package.json +++ b/package.json @@ -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/",