diff --git a/bin/stunnel.js b/bin/stunnel.js index a9c9da8..5fa50a3 100755 --- a/bin/stunnel.js +++ b/bin/stunnel.js @@ -5,6 +5,7 @@ var pkg = require('../package.json'); var program = require('commander'); +var url = require('url'); var stunnel = require('../wsclient.js'); function collectProxies(val, memo) { @@ -34,7 +35,9 @@ function collectProxies(val, memo) { , hostname: parts[1] , port: parts[2] }; - }).forEach(memo.push); + }).forEach(function (val) { + memo.push(val); + }); return memo; } @@ -46,25 +49,42 @@ program .action(function (url) { program.url = url; }) - .option('-k --insecure', 'Allow TLS connections to stunneld without valid certs (H)') + .option('-k --insecure', 'Allow TLS connections to stunneld without valid certs (rejectUnauthorized: false)') .option('--locals ', 'comma separated list of :: to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337', collectProxies, [ ]) // --reverse-proxies .option('--stunneld ', 'the domain (or ip address) at which you are running stunneld.js (the proxy)') // --proxy - .option('--secret', 'the same secret used by stunneld (used for JWT authentication)') - .option('--token', 'a pre-generated token for use with stunneld (instead of generating one with --secret)') + .option('--secret ', 'the same secret used by stunneld (used for JWT authentication)') + .option('--token ', 'a pre-generated token for use with stunneld (instead of generating one with --secret)') .parse(process.argv) ; -// Assumption: will not get next tcp packet unless previous packet succeeded -var hostname = 'aj.daplie.me'; // 'pokemap.hellabit.com' +program.stunneld = program.stunneld || 'wss://pokemap.hellabit.com:3000'; + var jwt = require('jsonwebtoken'); +var domainsMap = {}; +var tokenData = { + name: null +, domains: null +}; +var location = url.parse(program.stunneld); + +if (!location.protocol || /\./.test(location.protocol)) { + program.stunneld = 'wss://' + program.stunneld; + location = url.parse(program.stunneld); +} +program.stunneld = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : ''); + +program.locals.forEach(function (proxy) { + domainsMap[proxy.hostname] = true; +}); +tokenData.domains = Object.keys(domainsMap); +tokenData.name = tokenData.domains[0]; program.services = {}; program.locals.forEach(function (proxy) { //program.services = { 'ssh': 22, 'http': 80, 'https': 443 }; program.services[proxy.protocol] = proxy.port; }); -program.token = program.token || jwt.sign({ name: hostname }, program.secret || 'shhhhh'); -program.stunneld = program.stunneld || 'wss://pokemap.hellabit.com:3000'; +program.token = program.token || jwt.sign(tokenData, program.secret || 'shhhhh'); stunnel.connect(program); diff --git a/wsclient.js b/wsclient.js index f4fbce8..18078b9 100644 --- a/wsclient.js +++ b/wsclient.js @@ -4,6 +4,8 @@ var net = require('net'); var WebSocket = require('ws'); var sni = require('sni'); +var pack = require('tunnel-packer').pack; +var authenticated = false; // TODO move these helpers to tunnel-packer package function addrToId(address) { @@ -31,172 +33,169 @@ request.get('https://pokemap.hellabit.com:3000?access_token=' + token, { rejectU return; //*/ - function run(copts) { - var services = copts.services; // TODO pair with hostname / sni - var token = copts.token; - var tunnelUrl = copts.stunneld + '/?access_token=' + token; - var wstunneler; - var retry = true; - var localclients = {}; - wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: false }); +function run(copts) { + var services = copts.services; // TODO pair with hostname / sni + var token = copts.token; + var tunnelUrl = copts.stunneld + '/?access_token=' + token; + var wstunneler; + var retry = true; + var localclients = {}; + // BaaS / Backendless / noBackend / horizon.io + // user authentication + // a place to store data + // file management + // Synergy Teamwork Paradigm = Jabberwocky + var handlers = { + onmessage: function (opts) { + var cid = addrToId(opts); + var service = opts.service; + var port = services[service]; + var servername; + var str; + var m; - function onOpen() { - console.log('[open] tunneler connected'); + authenticated = true; - /* - setInterval(function () { - console.log(''); - console.log('localclients.length:', Object.keys(localclients).length); - console.log(''); - }, 5000); - */ + if (localclients[cid]) { + //console.log("[=>] received data from '" + cid + "' =>", opts.data.byteLength); + localclients[cid].write(opts.data); + return; + } + else if ('http' === service) { + str = opts.data.toString(); + m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); + servername = (m && m[1].toLowerCase() || '').split(':')[0]; + } + else if ('https' === service) { + servername = sni(opts.data); + } + else { + handlers._onLocalError(cid, opts, new Error("unsupported service '" + service + "'")); + return; + } - //wstunneler.send(token); + if (!servername) { + console.info("[error] missing servername for '" + cid + "'", opts.data.byteLength); + //console.warn(opts.data.toString()); + wstunneler.send(pack(opts, null, 'error'), { binary: true }); + return; + } - // BaaS / Backendless / noBackend / horizon.io - // user authentication - // a place to store data - // file management - // Synergy Teamwork Paradigm = Jabberwocky - var pack = require('tunnel-packer').pack; - var handlers = { - onmessage: function (opts) { - var cid = addrToId(opts); - console.log('[wsclient] onMessage:', cid); - var service = opts.service; - var port = services[service]; - var lclient; - var servername; - var str; - var m; + console.info("[connect] new client '" + cid + "' for '" + servername + "' (" + (handlers._numClients() + 1) + " clients)"); - function endWithError() { - try { - wstunneler.send(pack(opts, null, 'error'), { binary: true }); - } catch(e) { - // ignore - } - } - - if (localclients[cid]) { - console.log("[=>] received data from '" + cid + "' =>", opts.data.byteLength); - localclients[cid].write(opts.data); - return; - } - else if ('http' === service) { - str = opts.data.toString(); - m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); - servername = (m && m[1].toLowerCase() || '').split(':')[0]; - } - else if ('https' === service) { - servername = sni(opts.data); - } - else { - endWithError(); - return; - } - - if (!servername) { - console.warn("|__ERROR__| no servername found for '" + cid + "'", opts.data.byteLength); - //console.warn(opts.data.toString()); - wstunneler.send(pack(opts, null, 'error'), { binary: true }); - return; - } - - console.log("servername: '" + servername + "'"); - - lclient = localclients[cid] = net.createConnection({ port: port, host: '127.0.0.1' }, function () { - console.log("[=>] first packet from tunneler to '" + cid + "' as '" + opts.service + "'", opts.data.byteLength); - lclient.write(opts.data); - }); - lclient.on('data', function (chunk) { - console.log("[<=] local '" + opts.service + "' sent to '" + cid + "' <= ", chunk.byteLength, "bytes"); - //console.log(JSON.stringify(chunk.toString())); - wstunneler.send(pack(opts, chunk), { binary: true }); - }); - lclient.on('error', function (err) { - console.error("[error] local '" + opts.service + "' '" + cid + "'"); - console.error(err); - delete localclients[cid]; - try { - wstunneler.send(pack(opts, null, 'error'), { binary: true }); - } catch(e) { - // ignore - } - }); - lclient.on('end', function () { - console.log("[end] local '" + opts.service + "' '" + cid + "'"); - delete localclients[cid]; - try { - wstunneler.send(pack(opts, null, 'end'), { binary: true }); - } catch(e) { - // ignore - } - }); + localclients[cid] = net.createConnection({ port: port, host: '127.0.0.1' }, function () { + //console.log("[=>] first packet from tunneler to '" + cid + "' as '" + opts.service + "'", opts.data.byteLength); + localclients[cid].write(opts.data); + }); + localclients[cid].on('data', function (chunk) { + //console.log("[<=] local '" + opts.service + "' sent to '" + cid + "' <= ", chunk.byteLength, "bytes"); + //console.log(JSON.stringify(chunk.toString())); + wstunneler.send(pack(opts, chunk), { binary: true }); + }); + localclients[cid].on('error', function (err) { + handlers._onLocalError(cid, opts, err); + }); + localclients[cid].on('end', function () { + console.info("[end] closing client '" + cid + "' for '" + servername + "' (" + (handlers._numClients() - 1) + " clients)"); + handlers._onLocalClose(cid, opts); + }); + } + , onend: function (opts) { + var cid = addrToId(opts); + //console.log("[end] '" + cid + "'"); + handlers._onend(cid); + } + , onerror: function (opts) { + var cid = addrToId(opts); + //console.log("[error] '" + cid + "'", opts.code || '', opts.message); + handlers._onend(cid); + } + , _onend: function (cid) { + if (localclients[cid]) { + try { + localclients[cid].end(); + } catch(e) { + // ignore } - , onend: function (opts) { - var cid = addrToId(opts); - console.log("[end] '" + cid + "'"); - handlers._onend(cid); - } - , onerror: function (opts) { - var cid = addrToId(opts); - console.log("[error] '" + cid + "'", opts.code || '', opts.message); - handlers._onend(cid); - } - , _onend: function (cid) { - if (localclients[cid]) { - localclients[cid].end(); - } - delete localclients[cid]; - } - }; + } + delete localclients[cid]; + } + , _onLocalClose: function (cid, opts, err) { + try { + wstunneler.send(pack(opts, null, err && 'error' || 'end'), { binary: true }); + } catch(e) { + // ignore + } + delete localclients[cid]; + } + , _onLocalError: function (cid, opts, err) { + console.info("[error] closing '" + cid + "' because '" + err.message + "' (" + (handlers._numClients() - 1) + " clients)"); + handlers._onLocalClose(cid, opts, err); + } + , _numClients: function () { + return Object.keys(localclients).length; + } + }; + + var wsHandlers = { + onOpen: function () { + console.info("[open] connected to '" + copts.stunneld + "'"); + var machine = require('tunnel-packer').create(handlers); - wstunneler.on('message', machine.fns.addChunk); } - wstunneler.on('open', onOpen); + , onClose: function () { + if (!authenticated) { + console.info('[close] failed on first attempt... check authentication.'); + } + else if (retry) { + console.info('[retry] disconnected and waiting...'); + setTimeout(run, 5000, copts); + } + else { + console.info('[close] closing tunnel to exit...'); + } - wstunneler.on('close', function () { - console.log('closing tunnel...'); - process.removeListener('exit', onExit); - process.removeListener('SIGINT', onExit); + process.removeListener('exit', wsHandlers.onExit); + process.removeListener('SIGINT', wsHandlers.onExit); Object.keys(localclients).forEach(function (cid) { try { localclients[cid].end(); } catch(e) { // ignore } - delete localclients[cid]; }); + } - if (retry) { - console.log('retry on close'); - setTimeout(run, 5000); - } - }); - - wstunneler.on('error', function (err) { - console.error("[error] will retry on 'close'"); + , onError: function (err) { + console.error("[tunnel error] " + err.message); console.error(err); - }); + } - function onExit() { + , onExit: function () { retry = false; - console.log('on exit...'); try { wstunneler.close(); } catch(e) { + console.error("[error] wstunneler.close()"); console.error(e); // ignore } } + }; - process.on('exit', onExit); - process.on('SIGINT', onExit); - } + console.info("[connect] '" + copts.stunneld + "'"); + + wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !copts.insecure }); + wstunneler.on('open', wsHandlers.onOpen); + wstunneler.on('close', wsHandlers.onClose); + wstunneler.on('error', wsHandlers.onError); + process.on('exit', wsHandlers.onExit); + process.on('SIGINT', wsHandlers.onExit); +} + +module.exports.connect = run; - module.exports.connect = run; }());