38 Commits

Author SHA1 Message Date
Roman Shtylman
8efcb3a294 v1.8.2 2016-11-17 22:31:06 -08:00
Roman Shtylman
b9c1901d60 fix host header transform to support Host and host header 2016-11-17 22:28:34 -08:00
Roman Shtylman
edc182125f update request dep to 2.78 2016-11-17 22:27:57 -08:00
Sigve Sebastian Farstad
81c28d4d68 Fix typo in README.md (#143) 2016-09-06 06:58:25 -07:00
Roman Shtylman
9a1d48764a Merge pull request #113 from CircleCode/patch-1
[README] update the link to localtunnel server
2016-03-11 07:51:05 -08:00
Matthieu Codron
371db2870a [README] update the link to localtunnel server 2016-03-11 13:08:31 +01:00
Roman Shtylman
4ae493ae44 v1.8.1 2016-01-20 13:32:02 -08:00
Roman Shtylman
f487effe3a update History 2016-01-20 13:31:46 -08:00
Roman Shtylman
333af2b08f Merge pull request #108 from joshuaxls/master
Fix bug w/ HeaderHostTransformer and binary data
2016-01-20 13:29:50 -08:00
Joshua Slayton
b32041d8aa fix unnecessary binary->string conversion 2016-01-20 13:11:40 -08:00
Roman Shtylman
649de1b840 Merge pull request #107 from TooTallNate/patch-1
README: add missing back tick
2016-01-09 07:42:54 -08:00
Nathan Rajlich
f791217756 README: add missing back tick 2016-01-08 15:58:02 -08:00
Roman Shtylman
cce9d1490a update travis to node 4 and fix travis badge 2015-11-04 20:43:17 -08:00
Roman Shtylman
86cd2d3c58 v1.8.0 2015-11-04 08:04:54 -08:00
Roman Shtylman
d70c743014 update history 2015-11-04 08:04:29 -08:00
Roman Shtylman
4940043378 Merge pull request #102 from aronwoost/patch-01
Emit socket errors, so they can be handled
2015-11-04 07:54:45 -08:00
Aron Woost
b4a22bff64 Re-emit socket error 2015-11-04 12:04:07 +01:00
Roman Shtylman
176ec0479d Merge pull request #92 from cbas/master
Bumped deps to fix upstream SPDX license issues
2015-11-03 16:13:30 -08:00
Sebastiaan Deckers
fe316de3e0 Bumped deps to fix upstream SPDX license issues 2015-11-04 08:10:18 +08:00
Roman Shtylman
4f97434a69 v1.7.0 2015-07-22 17:19:32 -07:00
Roman Shtylman
a46cd02fcb update history 2015-07-22 17:19:15 -07:00
Roman Shtylman
792d9f19bd update history 2015-07-22 17:18:45 -07:00
Roman Shtylman
d0b483b92b Merge pull request #75 from artmees/master
Implement short argv via yargs
2015-07-22 17:16:49 -07:00
Ahmed Abdel Razzak
59d96a3cc6 Update the client to include shorthand options
Providing aliases for all of the supported options
-h, --host
-s, --subdomain
-l, --local-host
-o, --open
-p, --port
--help
--version

Minor clean up in the code removing the blocks that already handled
by yargs ( version, help and required options )

Handle validating that the passed port could be validated as a number

Remove the spaces in empty lines
2015-07-23 02:05:17 +02:00
Ahmed Abdel Razzak
b516ecccfa Replace optimist with yargs 2015-07-23 00:57:01 +02:00
Roman Shtylman
f68b1f06d9 1.6.0 2015-07-15 22:29:49 -07:00
Roman Shtylman
4d09875163 update history 2015-07-15 22:26:40 -07:00
Roman Shtylman
2773fe6923 Merge pull request #74 from davej/patch-1
Increase max event listeners to prevent premature memory-leak warnings
2015-07-15 22:24:47 -07:00
Roman Shtylman
457bd64ecc Merge pull request #85 from zeevl/keep-alive
Keep sockets alive after connecting
2015-07-15 15:19:35 -07:00
Steve Lamb
eb31659345 Keep sockets alive after connecting 2015-07-15 10:29:53 -07:00
Roman Shtylman
3ee8b1b884 Merge pull request #79 from Urucas/master
Add --open param to open tunnel url
2015-06-23 23:29:39 -07:00
vrunoa
15aac729bb add open param; opens url in your browser after localtunnel is launched
add open param; opens url in your browser after localtunnel is launched

update package version

multiplatform open with openurl package

update package.json

correct source code spacing & indentation

update openurl dependency

revert package version

move require('openurl') to the top, correct 4 spaces indentation
2015-06-23 17:10:28 -03:00
Dave Jeffery
e73cfe3e45 Increase max event listeners to prevent premature memory-leak warnings #71 2015-05-31 22:19:45 +01:00
Roman Shtylman
790a642a83 1.5.1 2015-05-26 09:28:59 -04:00
Roman Shtylman
e6539e1225 Merge pull request #72 from Lykathia/patch-license
package.json: Add license to package.json
2015-05-25 23:04:03 -04:00
Evan Lowry
4c0a5dc4eb Add license to package.json 2015-05-25 22:55:41 -03:00
Roman Shtylman
809262cf3d refactor into files 2014-12-21 17:46:38 -08:00
Roman Shtylman
ddb47d2f90 add .npmignore file 2014-10-25 17:21:59 -07:00
10 changed files with 392 additions and 325 deletions

1
.npmignore Normal file
View File

@@ -0,0 +1 @@
support

View File

@@ -1,3 +1,4 @@
language: node_js language: node_js
node_js: node_js:
- "0.10" - "4"
- "6"

View File

@@ -1,3 +1,25 @@
# 1.8.2 (2016-11-17)
* fix host header transform
* update request dependency
# 1.8.1 (2016-01-20)
* fix bug w/ HostHeaderTransformer and binary data
# 1.8.0 (2015-11-04)
* pass socket errors up to top level
# 1.7.0 (2015-07-22)
* add short arg options
# 1.6.0 (2015-05-15)
* keep sockets alive after connecting
* add --open param to CLI
# 1.5.0 (2014-10-25) # 1.5.0 (2014-10-25)
* capture all errors on remote socket and restart the tunnel * capture all errors on remote socket and restart the tunnel

View File

@@ -1,4 +1,6 @@
# localtunnel [![Build Status](https://travis-ci.org/defunctzombie/localtunnel.png?branch=master)](https://travis-ci.org/defunctzombie/localtunnel) # # localtunnel
[![Build Status](https://travis-ci.org/localtunnel/localtunnel.svg?branch=master)](https://travis-ci.org/localtunnel/localtunnel)
localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes. localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes.
@@ -42,13 +44,17 @@ Creates a new localtunnel to the specified local `port`. `fn` will be called onc
```javascript ```javascript
var localtunnel = require('localtunnel'); var localtunnel = require('localtunnel');
localtunnel(port, function(err, tunnel) { var tunnel = localtunnel(port, function(err, tunnel) {
if (err) ... if (err) ...
// the assigned public url for your tunnel // the assigned public url for your tunnel
// i.e. https://abcdefgjhij.localtunnel.me // i.e. https://abcdefgjhij.localtunnel.me
tunnel.url; tunnel.url;
}); });
tunnel.on('close', function() {
// tunnels are closed
});
``` ```
### opts ### opts
@@ -65,7 +71,7 @@ The `tunnel` instance returned to your callback emits the following events
|error|err|fires when an error happens on the tunnel| |error|err|fires when an error happens on the tunnel|
|close||fires when the tunnel has closed| |close||fires when the tunnel has closed|
The `tunnel instance has the following methods The `tunnel` instance has the following methods
|method|args|description| |method|args|description|
|----|----|----| |----|----|----|
@@ -79,7 +85,7 @@ Clients in other languages
## server ## ## server ##
See defunctzombie/localtunnel-server for details on the server that powers localtunnel. See [localtunnel/server](//github.com/localtunnel/server) for details on the server that powers localtunnel.
## License ## ## License ##
MIT MIT

View File

@@ -1,32 +1,38 @@
#!/usr/bin/env node #!/usr/bin/env node
var lt_client = require('../client'); var lt_client = require('../client');
var open_url = require('openurl');
var argv = require('optimist') var argv = require('yargs')
.usage('Usage: $0 --port [num]') .usage('Usage: $0 --port [num] <options>')
.options('host', { .option('h', {
default: 'http://localtunnel.me', alias: 'host',
describe: 'upstream server providing forwarding' describe: 'Upstream server providing forwarding',
default: 'http://localtunnel.me'
}) })
.options('subdomain', { .option('s', {
describe: 'request this subdomain' alias: 'subdomain',
describe: 'Request this subdomain'
}) })
.options('local-host', { .option('l', {
describe: 'tunnel traffic to this host instead of localhost, override Host header to this host' alias: 'local-host',
describe: 'Tunnel traffic to this host instead of localhost, override Host header to this host'
}) })
.options('version', { .options('o', {
describe: 'print version and exit' alias: 'open',
describe: 'opens url in your browser'
}) })
.describe('port', 'internal http server port') .option('p', {
alias: 'port',
describe: 'Internal http server port',
})
.require('port')
.help('help', 'Show this help and exit')
.version(require('../package').version)
.argv; .argv;
if (argv.version) { if (typeof argv.port !== 'number') {
console.log(require('../package.json').version); require('yargs').showHelp();
process.exit(0); console.error('port must be a number');
}
if (argv.port == null) {
require('optimist').showHelp();
console.error('Missing required arguments: port');
process.exit(1); process.exit(1);
} }
@@ -44,6 +50,10 @@ lt_client(opt.port, opt, function(err, tunnel) {
console.log('your url is: %s', tunnel.url); console.log('your url is: %s', tunnel.url);
if (argv.open) {
open_url.open(tunnel.url);
}
tunnel.on('error', function(err) { tunnel.on('error', function(err) {
throw err; throw err;
}); });

296
client.js
View File

@@ -1,301 +1,7 @@
var net = require('net');
var url = require('url');
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
var request = require('request');
var debug = require('debug')('localtunnel:client'); var debug = require('debug')('localtunnel:client');
var stream = require('stream'); var Tunnel = require('./lib/Tunnel');
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');
};
module.exports = function localtunnel(port, opt, fn) { module.exports = function localtunnel(port, opt, fn) {
if (typeof opt === 'function') { if (typeof opt === 'function') {

View File

@@ -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;
// after replacing the first instance of the Host header
// we just become a regular passthrough
if (!self.replaced) {
chunk = chunk.toString();
self.push(chunk.replace(/(\r\n[Hh]ost: )\S+/, function(match, $1) {
self.replaced = true;
return $1 + self.host;
}));
}
else {
self.push(chunk);
}
cb();
};
module.exports = HeaderHostTransformer;

157
lib/Tunnel.js Normal file
View File

@@ -0,0 +1,157 @@
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;
// increase max event listeners so that localtunnel consumers don't get
// warning messages as soon as they setup even one listener. See #71
self.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
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);
});
// re-emit socket error
tunnels.on('error', function(err) {
self.emit('error', err);
});
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;

123
lib/TunnelCluster.js Normal file
View File

@@ -0,0 +1,123 @@
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.setKeepAlive(true);
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;

View File

@@ -2,15 +2,17 @@
"author": "Roman Shtylman <shtylman@gmail.com>", "author": "Roman Shtylman <shtylman@gmail.com>",
"name": "localtunnel", "name": "localtunnel",
"description": "expose localhost to the world", "description": "expose localhost to the world",
"version": "1.5.0", "version": "1.8.2",
"license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/shtylman/localtunnel.git" "url": "git://github.com/localtunnel/localtunnel.git"
}, },
"dependencies": { "dependencies": {
"request": "2.11.4", "request": "2.78.0",
"optimist": "0.3.4", "yargs": "3.29.0",
"debug": "0.7.4" "debug": "2.2.0",
"openurl": "1.1.0"
}, },
"devDependencies": { "devDependencies": {
"mocha": "~1.17.0" "mocha": "~1.17.0"