24 Commits

Author SHA1 Message Date
Roman Shtylman
7b1fef982f 1.3.0 2014-05-13 20:33:41 -04:00
Roman Shtylman
b1ebef2b0b fix code style 2014-05-13 20:32:43 -04:00
Tymoteusz Paszun
9ad43778b7 fixed Host header replacement - remembers if header is already replaced and does not remove _transform method 2014-05-13 09:24:08 +02:00
Tymoteusz Paszun
887c444543 added test for chunked requests 2014-05-13 09:16:50 +02:00
Tymoteusz Paszun
828cb2afcb transform Host header when local-host is defined explicitly 2014-05-09 15:07:38 +02:00
Roman Shtylman
8768329fdd update history with two recent minor versions 2014-04-28 19:03:33 -04:00
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
6 changed files with 225 additions and 20 deletions

View File

@@ -1,3 +1,11 @@
# 1.2.0 / 2014-04-28
* return `client` from `localtunnel` API instantiation
# 1.1.0 / 2014-02-24
* add a host header transform to change the 'Host' header in requests
# 1.0.0 / 2014-02-14
* default to localltunnel.me for host

View File

@@ -14,7 +14,7 @@ This will install the localtunnel module globally and add the 'lt' client cli to
## 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
@@ -24,6 +24,13 @@ 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.
### 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 ##
The localtunnel client is also usable through an API (for test integration, automation, etc)
@@ -47,6 +54,7 @@ localtunnel(port, function(err, tunnel) {
### 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

View File

@@ -3,7 +3,6 @@ var lt_client = require('../client');
var argv = require('optimist')
.usage('Usage: $0 --port [num]')
.demand(['port'])
.options('host', {
default: 'http://localtunnel.me',
describe: 'upstream server providing forwarding'
@@ -12,29 +11,41 @@ var argv = require('optimist')
describe: 'request this subdomain'
})
.options('local-host', {
describe: 'tunnel traffic to this host instead of localhost'
describe: 'tunnel traffic to this host instead of localhost, override Host header to this host'
})
.options('version', {
describe: 'print version and exit'
})
.default('local-host', 'localhost')
.describe('port', 'internal http server port')
.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 = {
host: argv.host,
port: argv.port,
local_host: argv['local-host'],
subdomain: argv.subdomain,
}
};
lt_client(opt.port, opt, function(err, tunnel) {
if (err) {
console.error(err);
return process.exit(1);
throw err;
}
console.log('your url is: %s', tunnel.url);
tunnel.on('error', function(err) {
console.error(err);
throw err;
});
});

View File

@@ -5,6 +5,44 @@ 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)) {
@@ -28,7 +66,7 @@ TunnelCluster.prototype.open = function() {
var remote_host = opt.remote_host;
var remote_port = opt.remote_port;
var local_host = opt.local_host;
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);
@@ -55,13 +93,13 @@ TunnelCluster.prototype.open = function() {
});
function conn_local() {
debug('connecting locally to %s:%d', local_host, local_port);
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
@@ -71,19 +109,24 @@ TunnelCluster.prototype.open = function() {
});
function remote_close() {
debug('remote close');
self.emit('dead');
local.end();
};
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();
remote.removeListener('close', remote_close);
if (err.code !== 'ECONNREFUSED') {
return local.emit('error', err);
return remote.end();
}
// retrying connection to local server
@@ -93,7 +136,17 @@ TunnelCluster.prototype.open = function() {
local.once('connect', function() {
debug('connected locally');
remote.resume();
remote.pipe(local).pipe(remote);
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) {
@@ -105,8 +158,8 @@ TunnelCluster.prototype.open = function() {
// tunnel is considered open when remote connects
remote.once('connect', function() {
self.emit('open', remote);
conn_local();
});
remote.once('connect', conn_local);
};
var Tunnel = function(opt) {
@@ -153,6 +206,11 @@ Tunnel.prototype._init = function(cb) {
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;
@@ -173,7 +231,7 @@ Tunnel.prototype._establish = function(info) {
var self = this;
var opt = self._opt;
info.local_host = opt.local_host || 'localhost';
info.local_host = opt.local_host;
info.local_port = opt.port;
var tunnels = self.tunnel_cluster = TunnelCluster(info);
@@ -261,4 +319,5 @@ module.exports = function localtunnel(port, opt, fn) {
fn(null, client);
});
return client;
};

View File

@@ -2,7 +2,7 @@
"author": "Roman Shtylman <shtylman@gmail.com>",
"name": "localtunnel",
"description": "expose localhost to the world",
"version": "1.0.0",
"version": "1.3.0",
"repository": {
"type": "git",
"url": "git://github.com/shtylman/localtunnel.git"
@@ -16,7 +16,7 @@
"mocha": "~1.17.0"
},
"scripts": {
"test": "mocha --ui qunit --reporter list -- test/index.js"
"test": "mocha --ui qunit --reporter list --timeout 10000 -- test/index.js"
},
"bin": {
"lt": "./bin/client"

View File

@@ -8,7 +8,7 @@ var localtunnel = require('../');
test('setup local http server', function(done) {
var server = http.createServer();
server.on('request', function(req, res) {
res.write('foo');
res.write(req.headers.host);
res.end();
});
server.listen(function() {
@@ -51,7 +51,7 @@ test('query localtunnel server w/ ident', function(done) {
});
res.on('end', function() {
assert.equal('foo', body);
assert(/.*[.]localtunnel[.]me/.test(body), body);
done();
});
});
@@ -67,3 +67,122 @@ test('request specific domain', function(done) {
done();
});
});
suite('--local-host localhost');
test('setup localtunnel client', function(done) {
var opt = {
local_host: 'localhost'
};
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, 'localhost');
done();
});
});
req.end();
});
suite('--local-host 127.0.0.1');
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();
});
test('send chunked request', function(done) {
var uri = test._fake_url;
var parsed = url.parse(uri);
var opt = {
host: parsed.host,
port: 443,
headers: {
host: parsed.hostname,
'Transfer-Encoding': 'chunked'
},
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(require('crypto').randomBytes(1024 * 8).toString('base64'));
});