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' | ||||
| ``` | ||||
| 
 | ||||
| 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 | ||||
|  | ||||
							
								
								
									
										282
									
								
								bin/sclient.js
									
									
									
									
									
								
							
							
						
						
									
										282
									
								
								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,76 +18,245 @@ 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)(); | ||||
| 
 | ||||
| // arg 0 is node
 | ||||
| // arg 1 is sclient
 | ||||
| // arg 2 is remote
 | ||||
| // arg 3 is local
 | ||||
| if (4 !== process.argv.length) { | ||||
|   if (isPiped) { | ||||
|     local = ['|']; | ||||
|   } else { | ||||
|     usage(); | ||||
|     process.exit(1); | ||||
|   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 (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); | ||||
| } | ||||
| 
 | ||||
| // 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 '|'
 | ||||
|   localPort = parseInt(local[1], 10); | ||||
| } | ||||
| 
 | ||||
| if ('-' === localAddress || '|' === localAddress) { | ||||
|   // no need for port
 | ||||
| } else if (!localPort) { | ||||
|   usage(); | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| var opts = { | ||||
|   remoteAddr: remote[0] | ||||
| , remotePort: remote[1] | ||||
| , localAddress: localAddress | ||||
| , localPort: localPort | ||||
| , rejectUnauthorized: rejectUnauthorized | ||||
| , servername: servername | ||||
| }; | ||||
| require('../')(opts); | ||||
| main(); | ||||
|  | ||||
							
								
								
									
										61
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								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,28 +41,38 @@ 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) { | ||||
|   // Test connection first
 | ||||
|   var tlsOpts = { | ||||
|     host: opts.remoteAddr, port: opts.remotePort | ||||
|   , rejectUnauthorized: opts.rejectUnauthorized | ||||
|   }; | ||||
|   if (opts.servername) { | ||||
|     tlsOpts.servername = opts.servername; | ||||
|   } | ||||
|   var tlsSock = tls.connect(tlsOpts, function () { | ||||
|     tlsSock.end(); | ||||
|     listenForConns(opts); | ||||
|   }); | ||||
|   tlsSock.on('error', function (err) { | ||||
|     console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n'); | ||||
|     listenForConns(opts); | ||||
|   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(); | ||||
|       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; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "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.", | ||||
|   "main": "index.js", | ||||
|   "homepage": "https://telebit.cloud/sclient/", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user