diff --git a/README.md b/README.md index f4f383a..6f41eb5 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,17 @@ Creates a new localtunnel to the specified local `port`. `fn` will be called onc ```javascript var localtunnel = require('localtunnel'); -localtunnel(port, function(err, tunnel) { +var tunnel = localtunnel(port, function(err, tunnel) { if (err) ... // the assigned public url for your tunnel // i.e. https://abcdefgjhij.localtunnel.me tunnel.url; }); + +tunnel.on('close', function() { + // tunnels are closed +}); ``` ### opts diff --git a/client.js b/client.js index 8a81502..5dcf0ba 100644 --- a/client.js +++ b/client.js @@ -1,301 +1,7 @@ -var net = require('net'); -var url = require('url'); var EventEmitter = require('events').EventEmitter; - -var request = require('request'); var debug = require('debug')('localtunnel:client'); -var stream = require('stream'); -var util = require('util'); - -var Transform = stream.Transform; - -var HeaderHostTransformer = function(opts) { - if (!(this instanceof HeaderHostTransformer)) { - return new HeaderHostTransformer(opts); - } - - opts = opts || {} - Transform.call(this, opts); - - var self = this; - self.host = opts.host || 'localhost'; - self.replaced = false; -} -util.inherits(HeaderHostTransformer, Transform); - -HeaderHostTransformer.prototype._transform = function (chunk, enc, cb) { - var self = this; - chunk = chunk.toString(); - - // after replacing the first instance of the Host header - // we just become a regular passthrough - if (!self.replaced) { - self.push(chunk.replace(/(\r\nHost: )\S+/, function(match, $1) { - self.replaced = true; - return $1 + self.host; - })); - } - else { - self.push(chunk); - } - - cb(); -}; - -// manages groups of tunnels -var TunnelCluster = function(opt) { - if (!(this instanceof TunnelCluster)) { - return new TunnelCluster(opt); - } - - var self = this; - self._opt = opt; - - EventEmitter.call(self); -}; - -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 || 'localhost'; - 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.on('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)')); - } - - remote.end(); - }); - - function conn_local() { - if (remote.destroyed) { - debug('remote destroyed'); - self.emit('dead'); - return; - } - - 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() { - debug('remote close'); - self.emit('dead'); - local.end(); - }; - - remote.once('close', remote_close); - - // TODO some languages have single threaded servers which makes opening up - // multiple local connections impossible. We need a smarter way to scale - // and adjust for such instances to avoid beating on the door of the server - local.once('error', function(err) { - debug('local error %s', err.message); - local.end(); - - remote.removeListener('close', remote_close); - - if (err.code !== 'ECONNREFUSED') { - return remote.end(); - } - - // retrying connection to local server - setTimeout(conn_local, 1000); - }); - - local.once('connect', function() { - debug('connected locally'); - remote.resume(); - - var stream = remote; - - // if user requested specific local host - // then we use host header transform to replace the host header - if (opt.local_host) { - debug('transform Host header to %s', opt.local_host); - stream = remote.pipe(HeaderHostTransformer({ host: opt.local_host })); - } - - stream.pipe(local).pipe(remote); - - // when local closes, also get a new remote - local.once('close', function(had_error) { - debug('local connection closed [%s]', had_error); - }); - }); - } - - // tunnel is considered open when remote connects - remote.once('connect', function() { - self.emit('open', remote); - conn_local(); - }); -}; - -var Tunnel = function(opt) { - if (!(this instanceof Tunnel)) { - return new Tunnel(opt); - } - - var self = this; - self._closed = false; - self._opt = opt || {}; - - self._opt.host = self._opt.host || 'https://localtunnel.me'; -}; - -Tunnel.prototype.__proto__ = EventEmitter.prototype; - -// initialize connection -// callback with connection info -Tunnel.prototype._init = function(cb) { - var self = this; - var opt = self._opt; - - var params = { - path: '/', - json: true - }; - - var base_uri = opt.host + '/'; - - // optionally override the upstream server - var upstream = url.parse(opt.host); - - // no subdomain at first, maybe use requested domain - var assigned_domain = opt.subdomain; - - // where to quest - params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new'); - - (function get_url() { - request(params, function(err, res, body) { - if (err) { - // TODO (shtylman) don't print to stdout? - console.log('tunnel server offline: ' + err.message + ', retry 1s'); - return setTimeout(get_url, 1000); - } - - if (res.statusCode !== 200) { - var err = new Error((body && body.message) || 'localtunnel server returned an error, please try again'); - return cb(err); - } - - var port = body.port; - var host = upstream.hostname; - - var max_conn = body.max_conn_count || 1; - - cb(null, { - remote_host: upstream.hostname, - remote_port: body.port, - name: body.id, - url: body.url, - max_conn: max_conn - }); - }); - })(); -}; - -Tunnel.prototype._establish = function(info) { - var self = this; - var opt = self._opt; - - info.local_host = opt.local_host; - info.local_port = opt.port; - - var tunnels = self.tunnel_cluster = TunnelCluster(info); - - // only emit the url the first time - tunnels.once('open', function() { - self.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); - - var close_handler = function() { - tunnel.destroy(); - }; - - if (self._closed) { - return close_handler(); - } - - self.once('close', close_handler); - tunnel.once('close', function() { - self.removeListener('close', close_handler); - }); - }); - - // when a tunnel dies, open a new one - tunnels.on('dead', function(tunnel) { - tunnel_count--; - debug('tunnel dead [total: %d]', tunnel_count); - - if (self._closed) { - return; - } - - tunnels.open(); - }); - - // establish as many tunnels as allowed - for (var count = 0 ; count < info.max_conn ; ++count) { - tunnels.open(); - } -}; - -Tunnel.prototype.open = function(cb) { - var self = this; - - self._init(function(err, info) { - if (err) { - return cb(err); - } - - self.url = info.url; - self._establish(info); - cb(); - }); -}; - -// shutdown tunnels -Tunnel.prototype.close = function() { - var self = this; - - self._closed = true; - self.emit('close'); -}; +var Tunnel = require('./lib/Tunnel'); module.exports = function localtunnel(port, opt, fn) { if (typeof opt === 'function') { diff --git a/lib/HeaderHostTransformer.js b/lib/HeaderHostTransformer.js new file mode 100644 index 0000000..753fcf4 --- /dev/null +++ b/lib/HeaderHostTransformer.js @@ -0,0 +1,39 @@ +var stream = require('stream'); +var util = require('util'); + +var Transform = stream.Transform; + +var HeaderHostTransformer = function(opts) { + if (!(this instanceof HeaderHostTransformer)) { + return new HeaderHostTransformer(opts); + } + + opts = opts || {} + Transform.call(this, opts); + + var self = this; + self.host = opts.host || 'localhost'; + self.replaced = false; +} +util.inherits(HeaderHostTransformer, Transform); + +HeaderHostTransformer.prototype._transform = function (chunk, enc, cb) { + var self = this; + chunk = chunk.toString(); + + // after replacing the first instance of the Host header + // we just become a regular passthrough + if (!self.replaced) { + self.push(chunk.replace(/(\r\nHost: )\S+/, function(match, $1) { + self.replaced = true; + return $1 + self.host; + })); + } + else { + self.push(chunk); + } + + cb(); +}; + +module.exports = HeaderHostTransformer; diff --git a/lib/Tunnel.js b/lib/Tunnel.js new file mode 100644 index 0000000..f8ae007 --- /dev/null +++ b/lib/Tunnel.js @@ -0,0 +1,148 @@ +var url = require('url'); +var EventEmitter = require('events').EventEmitter; +var request = require('request'); +var debug = require('debug')('localtunnel:client'); + +var TunnelCluster = require('./TunnelCluster'); + +var Tunnel = function(opt) { + if (!(this instanceof Tunnel)) { + return new Tunnel(opt); + } + + var self = this; + self._closed = false; + self._opt = opt || {}; + + self._opt.host = self._opt.host || 'https://localtunnel.me'; +}; + +Tunnel.prototype.__proto__ = EventEmitter.prototype; + +// initialize connection +// callback with connection info +Tunnel.prototype._init = function(cb) { + var self = this; + var opt = self._opt; + + var params = { + path: '/', + json: true + }; + + var base_uri = opt.host + '/'; + + // optionally override the upstream server + var upstream = url.parse(opt.host); + + // no subdomain at first, maybe use requested domain + var assigned_domain = opt.subdomain; + + // where to quest + params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new'); + + (function get_url() { + request(params, function(err, res, body) { + if (err) { + // TODO (shtylman) don't print to stdout? + console.log('tunnel server offline: ' + err.message + ', retry 1s'); + return setTimeout(get_url, 1000); + } + + if (res.statusCode !== 200) { + var err = new Error((body && body.message) || 'localtunnel server returned an error, please try again'); + return cb(err); + } + + var port = body.port; + var host = upstream.hostname; + + var max_conn = body.max_conn_count || 1; + + cb(null, { + remote_host: upstream.hostname, + remote_port: body.port, + name: body.id, + url: body.url, + max_conn: max_conn + }); + }); + })(); +}; + +Tunnel.prototype._establish = function(info) { + var self = this; + var opt = self._opt; + + info.local_host = opt.local_host; + info.local_port = opt.port; + + var tunnels = self.tunnel_cluster = TunnelCluster(info); + + // only emit the url the first time + tunnels.once('open', function() { + self.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); + + var close_handler = function() { + tunnel.destroy(); + }; + + if (self._closed) { + return close_handler(); + } + + self.once('close', close_handler); + tunnel.once('close', function() { + self.removeListener('close', close_handler); + }); + }); + + // when a tunnel dies, open a new one + tunnels.on('dead', function(tunnel) { + tunnel_count--; + debug('tunnel dead [total: %d]', tunnel_count); + + if (self._closed) { + return; + } + + tunnels.open(); + }); + + // establish as many tunnels as allowed + for (var count = 0 ; count < info.max_conn ; ++count) { + tunnels.open(); + } +}; + +Tunnel.prototype.open = function(cb) { + var self = this; + + self._init(function(err, info) { + if (err) { + return cb(err); + } + + self.url = info.url; + self._establish(info); + cb(); + }); +}; + +// shutdown tunnels +Tunnel.prototype.close = function() { + var self = this; + + self._closed = true; + self.emit('close'); +}; + +module.exports = Tunnel; diff --git a/lib/TunnelCluster.js b/lib/TunnelCluster.js new file mode 100644 index 0000000..7634f6f --- /dev/null +++ b/lib/TunnelCluster.js @@ -0,0 +1,121 @@ +var EventEmitter = require('events').EventEmitter; +var debug = require('debug')('localtunnel:client'); +var net = require('net'); + +var HeaderHostTransformer = require('./HeaderHostTransformer'); + +// manages groups of tunnels +var TunnelCluster = function(opt) { + if (!(this instanceof TunnelCluster)) { + return new TunnelCluster(opt); + } + + var self = this; + self._opt = opt; + + EventEmitter.call(self); +}; + +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 || 'localhost'; + 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.on('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)')); + } + + remote.end(); + }); + + function conn_local() { + if (remote.destroyed) { + debug('remote destroyed'); + self.emit('dead'); + return; + } + + 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() { + debug('remote close'); + self.emit('dead'); + local.end(); + }; + + remote.once('close', remote_close); + + // TODO some languages have single threaded servers which makes opening up + // multiple local connections impossible. We need a smarter way to scale + // and adjust for such instances to avoid beating on the door of the server + local.once('error', function(err) { + debug('local error %s', err.message); + local.end(); + + remote.removeListener('close', remote_close); + + if (err.code !== 'ECONNREFUSED') { + return remote.end(); + } + + // retrying connection to local server + setTimeout(conn_local, 1000); + }); + + local.once('connect', function() { + debug('connected locally'); + remote.resume(); + + var stream = remote; + + // if user requested specific local host + // then we use host header transform to replace the host header + if (opt.local_host) { + debug('transform Host header to %s', opt.local_host); + stream = remote.pipe(HeaderHostTransformer({ host: opt.local_host })); + } + + stream.pipe(local).pipe(remote); + + // when local closes, also get a new remote + local.once('close', function(had_error) { + debug('local connection closed [%s]', had_error); + }); + }); + } + + // tunnel is considered open when remote connects + remote.once('connect', function() { + self.emit('open', remote); + conn_local(); + }); +}; + +module.exports = TunnelCluster;