diff --git a/client.js b/client.js index 404b5a8..715dd28 100644 --- a/client.js +++ b/client.js @@ -2,28 +2,119 @@ var net = require('net'); var url = require('url'); var EventEmitter = require('events').EventEmitter; -var after = require('after'); var request = require('request'); +var debug = require('debug')('localtunnel:client'); -// request upstream url and connection info -var request_url = function(params, cb) { - request(params, function(err, res, body) { - if (err) { - return cb(err); - } +// manages groups of tunnels +var TunnelCluster = function(opt) { + if (!(this instanceof TunnelCluster)) { + return new TunnelCluster(opt); + } - cb(null, body); - }); + var self = this; + self._opt = opt; + + EventEmitter.call(self); }; -var connect = function(opt) { +TunnelCluster.prototype.__proto__ = EventEmitter.prototype; + +// establish a new tunnel +TunnelCluster.prototype.open = function() { + var self = this; + + var opt = self._opt || {}; + + var remote_host = opt.remote_host; + var remote_port = opt.remote_port; + + var local_host = opt.local_host; + var local_port = opt.local_port; + + debug('establishing tunnel %s:%s <> %s:%s', local_host, local_port, remote_host, remote_port); + + // connection to localtunnel server + var remote = net.connect({ + host: remote_host, + port: remote_port + }); + + remote.once('error', function(err) { + // emit connection refused errors immediately, because they + // indicate that the tunnel can't be established. + if (err.code === 'ECONNREFUSED') { + self.emit('error', new Error('connection refused: ' + remote_host + ':' + remote_port + ' (check your firewall settings)')); + } + else { + self.emit('error', err); + } + + setTimeout(function() { + self.emit('dead'); + }, 1000); + }); + + function conn_local() { + debug('connecting locally to %s:%d', local_host, local_port); + + remote.pause(); + + // connection to local http server + var local = net.connect({ + host: local_host, + port: local_port + }); + + function remote_close() { + self.emit('dead'); + local.unpipe(); + local.end(); + }; + + remote.once('close', remote_close); + + local.on('error', function(err) { + local.end(); + + remote.removeListener('close', remote_close); + + if (err.code !== 'ECONNREFUSED') { + return local.emit('error', err); + } + + // retrying connection to local server + setTimeout(conn_local, 1000); + }); + + local.once('connect', function() { + debug('connected locally'); + remote.resume(); + remote.pipe(local).pipe(remote); + + // when local closes, also get a new remote + local.once('close', function(had_error) { + remote.unpipe(); + local.unpipe(); + remote.end(); + debug('local connection closed [%s]', had_error); + }); + }); + } + + // tunnel is considered open when remote connects + remote.once('connect', function() { + self.emit('open'); + }); + remote.once('connect', conn_local); +}; + +var init = function(opt, cb) { var ev = new EventEmitter(); - // local host - var local_host = opt.local_host; - - // local port - var local_port = opt.port; + var params = { + path: '/', + json: true + }; var base_uri = opt.host + '/'; @@ -33,134 +124,68 @@ var connect = function(opt) { // no subdomain at first, maybe use requested domain var assigned_domain = opt.subdomain; - // connect to upstream given connection parameters - var tunnel = function (remote_host, remote_port, dead) { - - var remote_opt = { - host: remote_host, - port: remote_port - }; - - var local_opt = { - host: local_host, - port: local_port - }; - - var remote_attempts = 0; - - (function conn(conn_had_error) { - if (conn_had_error) { - return; - } - - // we need a new tunnel - if (++remote_attempts >= 3) { - return dead(); - } - - // connection to localtunnel server - var remote = net.connect(remote_opt); - - remote.once('error', function(err) { - - // emit connection refused errors immediately, because they - // indicate that the tunnel can't be established. - if (err.code === 'ECONNREFUSED') { - ev.emit('error', new Error('connection refused: ' + remote_host + ':' + remote_port + ' (check your firewall settings)')); - } - else { - remote.emit('error', err); - } - - // retrying connection to local server - setTimeout(conn, 1000); - }); - - function recon_local() { - remote.pause(); - remote_attempts = 0; - - // connection to local http server - var local = net.connect(local_opt); - - local.once('error', function(err) { - if (err.code !== 'ECONNREFUSED') { - local.emit('error', err); - } - - // retrying connection to local server - setTimeout(recon_local, 1000); - }); - - local.once('connect', function() { - remote.resume(); - remote.pipe(local).pipe(remote, {end: false}); - }); - - local.once('close', function(had_error) { - if (had_error) { - return; - } - recon_local(); - }); - } - - remote.once('close', conn); - remote.once('connect', recon_local); - })(); - }; - - var params = { - path: '/', - json: true - }; - // where to quest params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new'); - function init_tunnel() { - // get an id from lt server and setup forwarding tcp connections - request_url(params, function(err, body) { + (function get_url() { + request(params, function(err, res, body) { if (err) { - ev.emit('error', new Error('tunnel server not available: ' + err.message + ', retry 1s')); + ev.emit('error', new Error('tunnel server offline: ' + err.message + ', retry 1s')); - // retry interval for id request - return setTimeout(function() { - init_tunnel(); - }, 1000); + return setTimeout(get_url, 1000); } - // our assigned hostname and tcp port var port = body.port; var host = upstream.hostname; - // store the id so we can try to get the same one - assigned_domain = body.id; - var max_conn = body.max_conn_count || 1; - // after all our tunnels die, we ask for new ones - // this might happen if the upstream server dies - var dead = after(max_conn, function() { - init_tunnel(); + cb(null, { + remote_host: upstream.hostname, + remote_port: body.port, + name: body.id, + url: body.url, + max_conn: max_conn }); - - for (var count = 0 ; count < max_conn ; ++count) { - tunnel(host, port, dead); - } - - ev.emit('url', body.url); }); - } - - init_tunnel(); + })(); return ev; }; -module.exports.connect = connect; +module.exports.connect = function(opt) { + var client = init(opt, function(err, info) { -// for backwards compatibility -// old localtunnel modules had server and client code in same module -// so to keep .client working we expose it here -module.exports.client = module.exports; + info.local_host = opt.local_host; + info.local_port = opt.port; + + var tunnels = TunnelCluster(info); + + // only emit the url the first time + tunnels.once('open', function() { + client.emit('url', info.url); + }); + + var tunnel_count = 0; + + // track open count + tunnels.on('open', function(tunnel) { + tunnel_count++; + debug('tunnel open [total: %d]', tunnel_count); + }); + + // when a tunnel dies, open a new one + tunnels.on('dead', function() { + tunnel_count--; + debug('tunnel dead [total: %d]', tunnel_count); + tunnels.open(); + }); + + // establish as many tunnels as allowed + for (var count = 0 ; count < info.max_conn ; ++count) { + tunnels.open(); + } + }); + + return client; +}; diff --git a/package.json b/package.json index 32f1c26..15dd2eb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dependencies": { "request": "2.11.4", "optimist": "0.3.4", - "after": "0.8.1" + "debug": "0.7.4" }, "devDependencies": {}, "bin": {