26 Commits

Author SHA1 Message Date
Roman Shtylman
3026d6a42c 1.2.0 2014-04-28 19:00:12 -04:00
Roman Shtylman
abd461f83a Merge pull request #47 from dscape/patch-1
return client from 'localtunnel' api call
2014-04-28 18:58:54 -04:00
Nuno Job
2acea3d77f Return client
This allows manipulating the client from outside. Allowing, for example, to close a connection.
2014-04-28 23:32:00 +01:00
Roman Shtylman
5d0eb3382a add --version CLI flag to get version info 2014-04-22 19:55:19 -04:00
Roman Shtylman
3b67c8a8ce 1.1.2 2014-04-20 10:51:24 -04:00
Kevin Ingersoll
71552a336e Increase default Mocha timeout 2014-04-20 10:45:21 -04:00
Roman Shtylman
87a23bf28c fix status code check for url request 2014-04-19 19:33:52 -04:00
Roman Shtylman
3d54de851f handle errors from localtunnel server when requesting initial url 2014-04-19 19:30:44 -04:00
Roman Shtylman
92bb807908 fix typo
fixes #45
2014-04-18 09:34:48 -04:00
Roman Shtylman
afbdc3697e 1.1.1 2014-04-15 09:33:34 -04:00
Roman Shtylman
0049f21b55 Merge pull request #38 from LinusU/patch-1
re-throw client errors from bin/lt to let node handle it
2014-04-14 19:27:56 -04:00
Roman Shtylman
509841104b fix for RangeError stack size exceeded
This error would happen when there was a problem connecting to the local
server. The local.on('error') handler should have been a 'once' handler
because we emit the error again if it isn't a CONNREFUSED. So in the
case of a CONNRESET, it would trigger an infinite loop since the error
was being emitted back onto the local variable. Instead we just close
the remote socket and let a new one takes its place.

fixes #36
2014-04-14 15:36:42 -04:00
Linus Unnebäck
92caf2f204 lt: better error handling
Let node handle the displaying of error and setting exit code.
2014-04-14 21:15:11 +02:00
Roman Shtylman
9487797e02 1.1.0 2014-02-24 19:51:14 -05:00
Roman Shtylman
a42f6a8d8d document local-host option 2014-02-24 19:49:40 -05:00
Roman Shtylman
14b4bcb96f add tests for host header transform 2014-02-24 19:43:34 -05:00
Roman Shtylman
4aa65002eb use host header transform only when local-host is specified 2014-02-24 19:43:07 -05:00
Fredi Pevcin
08676ba81d Set Host header accordingly to local-host option 2014-02-24 00:26:38 +01:00
Roman Shtylman
174e7f3982 1.0.0 2014-02-14 00:37:45 -05:00
Roman Shtylman
44be55cd7b readme: fix markdown table
[ci skip]
2014-02-14 00:36:53 -05:00
Roman Shtylman
5c6cd2359c add travis badge
- remove node 0.8
- add node 0.10
2014-02-14 00:34:40 -05:00
Roman Shtylman
2f6f9459ad change main export signature to localtunnel(port, opt, fn)
Makes for a simpler hello world app
2014-02-14 00:32:43 -05:00
Roman Shtylman
7217a08a05 add history.md to track changes 2014-02-13 23:52:12 -05:00
Roman Shtylman
fbfc923a7e remove connect export in favor of single function
Since connect was the only function exported, we can just export the
function directly to make things simpler.
2014-02-13 23:51:29 -05:00
Roman Shtylman
d9bc11b520 default host to localtuunel.me
If no host specified, then default to localtunnel.me

close #31
2014-02-13 23:43:54 -05:00
Roman Shtylman
ad64611bd1 add tests 2014-02-13 23:41:49 -05:00
7 changed files with 270 additions and 41 deletions

View File

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

7
History.md Normal file
View File

@@ -0,0 +1,7 @@
# 1.0.0 / 2014-02-14
* default to localltunnel.me for host
* remove exported `connect` method (just export one function that does the same thing)
* change localtunnel signature to (port, opt, fn)
# 0.2.2 / 2014-01-09

View File

@@ -1,4 +1,4 @@
# localtunnel # # localtunnel [![Build Status](https://travis-ci.org/defunctzombie/localtunnel.png?branch=master)](https://travis-ci.org/defunctzombie/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.
@@ -14,7 +14,7 @@ This will install the localtunnel module globally and add the 'lt' client cli to
## use ## ## use ##
Super Easy! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel. Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
``` ```
lt --port 8000 lt --port 8000
@@ -24,31 +24,53 @@ Thats it! It will connect to the tunnel server, setup the tunnel, and tell you w
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back. You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
### arguments
Below are some common arguments. See `lt --help` for additional arguments
* `--subdomain` request a named subdomain on the localtunnel server (default is random characters)
* `--local-host` proxy to a hostname other than localhost
## API ## ## API ##
The localtunnel client is also usable through an API (for test integration, automation, etc) The localtunnel client is also usable through an API (for test integration, automation, etc)
### localtunnel(port [,opts], fn)
Creates a new localtunnel to the specified local `port`. `fn` will be called once you have been assigned a public localtunnel url. `opts` can be used to request a specific `subdomain`.
```javascript ```javascript
var localtunnel = require('localtunnel'); var localtunnel = require('localtunnel');
var client = localtunnel.connect({ localtunnel(port, function(err, tunnel) {
// the localtunnel server if (err) ...
host: 'http://localtunnel.me',
// your local application port
port: 12345
});
// when your are assigned a url // the assigned public url for your tunnel
client.on('url', function(url) { // i.e. https://abcdefgjhij.localtunnel.me
// you can now make http requests to the url tunnel.url;
// they will be proxied to your local server on port [12345]
});
client.on('error', function(err) {
// uh oh!
}); });
``` ```
### opts
* `subdomain` A *string* value requesting a specific subdomain on the proxy server. **Note** You may not actually receive this name depending on availablily.
* `local_host` Proxy to this hostname instead of `localhost`. This will also cause the `Host` header to be re-written to this value in proxied requests.
### Tunnel
The `tunnel` instance returned to your callback emits the following events
|event|args|description|
|----|----|----|
|error|err|fires when an error happens on the tunnel|
|close||fires when the tunnel has closed|
The `tunnel instance has the following methods
|method|args|description|
|----|----|----|
|close||close the tunnel|
## other clients ## ## other clients ##
Clients in other languages Clients in other languages
@@ -57,7 +79,7 @@ Clients in other languages
## server ## ## server ##
See shtylman/localtunnel-server for details on the server that powers localtunnel. See defunctzombie/localtunnel-server for details on the server that powers localtunnel.
## License ## ## License ##
MIT MIT

View File

@@ -3,7 +3,6 @@ var lt_client = require('../client');
var argv = require('optimist') var argv = require('optimist')
.usage('Usage: $0 --port [num]') .usage('Usage: $0 --port [num]')
.demand(['port'])
.options('host', { .options('host', {
default: 'http://localtunnel.me', default: 'http://localtunnel.me',
describe: 'upstream server providing forwarding' describe: 'upstream server providing forwarding'
@@ -14,26 +13,41 @@ var argv = require('optimist')
.options('local-host', { .options('local-host', {
describe: 'tunnel traffic to this host instead of localhost' describe: 'tunnel traffic to this host instead of localhost'
}) })
.options('version', {
describe: 'print version and exit'
})
.default('local-host', 'localhost') .default('local-host', 'localhost')
.describe('port', 'internal http server port') .describe('port', 'internal http server port')
.argv; .argv;
if (argv.version) {
console.log(require('../package.json').version);
process.exit(0);
}
if (argv.port == null) {
require('optimist').showHelp();
console.error('Missing required arguments: port');
process.exit(1);
}
var opt = { var opt = {
host: argv.host, host: argv.host,
port: argv.port, port: argv.port,
local_host: argv['local-host'], local_host: argv['local-host'],
subdomain: argv.subdomain, subdomain: argv.subdomain,
} };
var client = lt_client.connect(opt); lt_client(opt.port, opt, function(err, tunnel) {
if (err) {
throw err;
}
// only emitted when the url changes console.log('your url is: %s', tunnel.url);
client.on('url', function(url) {
console.log('your url is: %s', url);
});
client.on('error', function(err) { tunnel.on('error', function(err) {
console.error(err); throw err;
});
}); });
// vim: ft=javascript // vim: ft=javascript

View File

@@ -5,6 +5,38 @@ var EventEmitter = require('events').EventEmitter;
var request = require('request'); var request = require('request');
var debug = require('debug')('localtunnel:client'); 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
self.push(chunk.replace(/(\r\nHost: )\S+/, function(match, $1) {
self._transform = undefined;
return $1 + self.host;
}));
cb();
};
// manages groups of tunnels // manages groups of tunnels
var TunnelCluster = function(opt) { var TunnelCluster = function(opt) {
if (!(this instanceof TunnelCluster)) { if (!(this instanceof TunnelCluster)) {
@@ -55,13 +87,13 @@ TunnelCluster.prototype.open = function() {
}); });
function conn_local() { function conn_local() {
debug('connecting locally to %s:%d', local_host, local_port);
if (remote.destroyed) { if (remote.destroyed) {
debug('remote destroyed');
self.emit('dead'); self.emit('dead');
return; return;
} }
debug('connecting locally to %s:%d', local_host, local_port);
remote.pause(); remote.pause();
// connection to local http server // connection to local http server
@@ -71,19 +103,24 @@ TunnelCluster.prototype.open = function() {
}); });
function remote_close() { function remote_close() {
debug('remote close');
self.emit('dead'); self.emit('dead');
local.end(); local.end();
}; };
remote.once('close', remote_close); remote.once('close', remote_close);
local.on('error', function(err) { // 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(); local.end();
remote.removeListener('close', remote_close); remote.removeListener('close', remote_close);
if (err.code !== 'ECONNREFUSED') { if (err.code !== 'ECONNREFUSED') {
return local.emit('error', err); return remote.end();
} }
// retrying connection to local server // retrying connection to local server
@@ -93,7 +130,16 @@ TunnelCluster.prototype.open = function() {
local.once('connect', function() { local.once('connect', function() {
debug('connected locally'); debug('connected locally');
remote.resume(); remote.resume();
remote.pipe(local).pipe(remote);
var stream = remote;
// if user requested something other than localhost
// then we use host header transform to replace the host header
if (local_host !== 'localhost') {
stream = remote.pipe(HeaderHostTransformer({ host: local_host }));
}
stream.pipe(local).pipe(remote);
// when local closes, also get a new remote // when local closes, also get a new remote
local.once('close', function(had_error) { local.once('close', function(had_error) {
@@ -105,8 +151,8 @@ TunnelCluster.prototype.open = function() {
// tunnel is considered open when remote connects // tunnel is considered open when remote connects
remote.once('connect', function() { remote.once('connect', function() {
self.emit('open', remote); self.emit('open', remote);
conn_local();
}); });
remote.once('connect', conn_local);
}; };
var Tunnel = function(opt) { var Tunnel = function(opt) {
@@ -116,7 +162,9 @@ var Tunnel = function(opt) {
var self = this; var self = this;
self._closed = false; self._closed = false;
self._opt = opt; self._opt = opt || {};
self._opt.host = self._opt.host || 'https://localtunnel.me';
}; };
Tunnel.prototype.__proto__ = EventEmitter.prototype; Tunnel.prototype.__proto__ = EventEmitter.prototype;
@@ -151,6 +199,11 @@ Tunnel.prototype._init = function(cb) {
return setTimeout(get_url, 1000); 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 port = body.port;
var host = upstream.hostname; var host = upstream.hostname;
@@ -220,15 +273,17 @@ Tunnel.prototype._establish = function(info) {
} }
}; };
Tunnel.prototype.open = function() { Tunnel.prototype.open = function(cb) {
var self = this; var self = this;
self._init(function(err, info) { self._init(function(err, info) {
if (err) { if (err) {
return self.emit('error', err); return cb(err);
} }
self.url = info.url;
self._establish(info); self._establish(info);
cb();
}); });
}; };
@@ -240,8 +295,22 @@ Tunnel.prototype.close = function() {
self.emit('close'); self.emit('close');
}; };
module.exports.connect = function(opt) { module.exports = function localtunnel(port, opt, fn) {
if (typeof opt === 'function') {
fn = opt;
opt = {};
}
opt = opt || {};
opt.port = port;
var client = Tunnel(opt); var client = Tunnel(opt);
client.open(); client.open(function(err) {
if (err) {
return fn(err);
}
fn(null, client);
});
return client; return client;
}; };

View File

@@ -2,7 +2,7 @@
"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": "0.2.2", "version": "1.2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/shtylman/localtunnel.git" "url": "git://github.com/shtylman/localtunnel.git"
@@ -12,7 +12,12 @@
"optimist": "0.3.4", "optimist": "0.3.4",
"debug": "0.7.4" "debug": "0.7.4"
}, },
"devDependencies": {}, "devDependencies": {
"mocha": "~1.17.0"
},
"scripts": {
"test": "mocha --ui qunit --reporter list --timeout 10000 -- test/index.js"
},
"bin": { "bin": {
"lt": "./bin/client" "lt": "./bin/client"
}, },

113
test/index.js Normal file
View File

@@ -0,0 +1,113 @@
var http = require('http');
var https = require('https');
var url = require('url');
var assert = require('assert');
var localtunnel = require('../');
test('setup local http server', function(done) {
var server = http.createServer();
server.on('request', function(req, res) {
res.write(req.headers.host);
res.end();
});
server.listen(function() {
var port = server.address().port;
test._fake_port = port;
console.log('local http on:', port);
done();
});
});
test('setup localtunnel client', function(done) {
localtunnel(test._fake_port, function(err, tunnel) {
assert.ifError(err);
assert.ok(new RegExp('^https:\/\/.*localtunnel.me' + '$').test(tunnel.url));
test._fake_url = tunnel.url;
done();
});
});
test('query localtunnel server w/ ident', function(done) {
var uri = test._fake_url;
var parsed = url.parse(uri);
var opt = {
host: parsed.host,
port: 443,
headers: {
host: parsed.hostname
},
path: '/'
};
var req = https.request(opt, function(res) {
res.setEncoding('utf8');
var body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
assert(/.*[.]localtunnel[.]me/.test(body), body);
done();
});
});
req.end();
});
test('request specific domain', function(done) {
localtunnel(test._fake_port, { subdomain: 'abcd' }, function(err, tunnel) {
assert.ifError(err);
assert.ok(new RegExp('^https:\/\/abcd.localtunnel.me' + '$').test(tunnel.url));
tunnel.close();
done();
});
});
suite('local-host');
test('setup localtunnel client', function(done) {
var opt = {
local_host: '127.0.0.1'
};
localtunnel(test._fake_port, opt, function(err, tunnel) {
assert.ifError(err);
assert.ok(new RegExp('^https:\/\/.*localtunnel.me' + '$').test(tunnel.url));
test._fake_url = tunnel.url;
done();
});
});
test('override Host header with local-host', function(done) {
var uri = test._fake_url;
var parsed = url.parse(uri);
var opt = {
host: parsed.host,
port: 443,
headers: {
host: parsed.hostname
},
path: '/'
};
var req = https.request(opt, function(res) {
res.setEncoding('utf8');
var body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
assert.equal(body, '127.0.0.1');
done();
});
});
req.end();
});