26 Commits

Author SHA1 Message Date
Roman Shtylman
ac70515143 0.2.2 2014-01-09 11:07:18 -05:00
Roman Shtylman
8d7ccccf21 remove local.unpipe() on remote close
This will happen automatically.

close #28
2014-01-09 11:06:58 -05:00
Roman Shtylman
77091b3d93 0.2.1 2013-12-31 17:34:04 -05:00
Roman Shtylman
4f4a147b45 don't unpipe on local close
Pipe will do this for us
2013-12-31 17:33:49 -05:00
Roman Shtylman
f1d809a84d 0.2.0 2013-12-31 15:39:16 -05:00
Roman Shtylman
eba003bd26 add a .close method to shutdown the tunnel 2013-12-31 15:38:45 -05:00
Roman Shtylman
3354c4c6e3 rework tunnel logic
Refactoring to make things a bit saner and easier to debug.
2013-12-05 11:26:19 -05:00
Roman Shtylman
1c2757e604 Merge pull request #27 from adammck/couldnt-establish-tunnel
Add more verbose error for ECONNREFUSED
2013-11-20 09:34:48 -08:00
Adam Mckaig
790e55e881 Add more verbose error for ECONNREFUSED
If the tunnel server can be reached (at e.g. http://localtunnel.me/?new)
but the tunnel (to e.g. grpi.localtunnel.me:44827) can't actually be
established, the client currently gets stuck in a loop retrying forever
with no indication as to what's wrong. This doesn't fix the loop, since
it does seem desirable to retry forever, but logs:

    [Error: connection refused: localtunnel.me:44827]
2013-11-20 12:29:31 -05:00
Roman Shtylman
a9b0274ff4 0.1.3 2013-11-14 12:10:02 -05:00
Roman Shtylman
83ecb29eff Merge pull request #26 from EverythingMe/override_localhost
Added the --localhost parameter to tunnel the traffic to other hosts
2013-11-14 09:08:47 -08:00
Omri Bahumi
21df257d16 Added the --local-host parameter to tunnel the traffic to other hosts 2013-11-14 18:06:19 +02:00
Roman Shtylman
18ada0854a 0.1.2 2013-11-06 23:25:28 -05:00
Roman Shtylman
34afd6537d more resilient to upstream server failure and restart 2013-11-06 23:25:05 -05:00
Roman Shtylman
2c38aefb9d add go client to readme 2013-10-23 11:31:28 -04:00
Roman Shtylman
aa488f6e76 0.1.1 2013-10-22 15:57:36 -04:00
Roman Shtylman
f6618953f9 Merge pull request #20 from eagleeye/master
Do not call success callback right after error in request_url
2013-10-16 18:49:48 -07:00
Andrii Shumada
092d050fa0 Do not call success callback right after error in request_url 2013-10-16 12:05:37 +03:00
Roman Shtylman
0334ace20b 0.1.0 2013-06-17 02:13:17 -04:00
Roman Shtylman
13afcff1ae fix README api example 2013-06-17 02:12:54 -04:00
Roman Shtylman
ed5aa3f16b remove server components
moved to localtunnel-server repo
2013-06-17 02:11:41 -04:00
Roman Shtylman
2fcac1336c add debug module to deps 2013-06-16 18:35:55 -04:00
Roman Shtylman
0568ae0bef close client tcp sockets after each http response
While a little less efficient than keeping tcp connections open, this
helps ensure that bad things don't happen on the socket connections when
http protocol issues happen.
2013-06-16 18:24:27 -04:00
Roman Shtylman
585a8afad7 fix undefined variable
fixes #8
2013-02-11 14:19:34 -05:00
Roman Shtylman
fbe841a1c5 add node 0.9 for travis testing 2013-01-26 13:01:16 -05:00
Roman Shtylman
929473913f add notes about running your own server 2012-12-24 03:14:19 -05:00
11 changed files with 251 additions and 713 deletions

View File

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

View File

@@ -1,4 +1,4 @@
# localtunnel [![Build Status](https://secure.travis-ci.org/shtylman/localtunnel.png)](http://travis-ci.org/shtylman/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.
@@ -10,6 +10,8 @@ Great for working with browser testing tools like browserling or external api ca
npm install -g localtunnel npm install -g localtunnel
``` ```
This will install the localtunnel module globally and add the 'lt' client cli tool to your PATH.
## use ## ## use ##
Super Easy! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel. Super Easy! Assuming your local server is running on port 8000, just use the ```lt``` command to start the tunnel.
@@ -24,12 +26,12 @@ You can restart your local server all you want, ```lt``` is smart enough to dete
## API ## ## API ##
The localtunnel client is also usable through an API (test integration, automation, etc) The localtunnel client is also usable through an API (for test integration, automation, etc)
```javascript ```javascript
var lt_client = require('localtunnel').client; var localtunnel = require('localtunnel');
var client = lt_client.connect({ var client = localtunnel.connect({
// the localtunnel server // the localtunnel server
host: 'http://localtunnel.me', host: 'http://localtunnel.me',
// your local application port // your local application port
@@ -46,3 +48,16 @@ client.on('error', function(err) {
// uh oh! // uh oh!
}); });
``` ```
## other clients ##
Clients in other languages
*go* [gotunnelme](https://github.com/NoahShen/gotunnelme)
## server ##
See shtylman/localtunnel-server for details on the server that powers localtunnel.
## License ##
MIT

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
var lt_client = require(__dirname + '/../client'); var lt_client = require('../client');
var argv = require('optimist') var argv = require('optimist')
.usage('Usage: $0 --port [num]') .usage('Usage: $0 --port [num]')
@@ -11,12 +11,17 @@ var argv = require('optimist')
.options('subdomain', { .options('subdomain', {
describe: 'request this subdomain' describe: 'request this subdomain'
}) })
.options('local-host', {
describe: 'tunnel traffic to this host instead of localhost'
})
.default('local-host', 'localhost')
.describe('port', 'internal http server port') .describe('port', 'internal http server port')
.argv; .argv;
var opt = { var opt = {
host: argv.host, host: argv.host,
port: argv.port, port: argv.port,
local_host: argv['local-host'],
subdomain: argv.subdomain, subdomain: argv.subdomain,
} }

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env node
// vendor
var log = require('book');
var optimist = require('optimist');
var argv = optimist
.usage('Usage: $0 --port [num]')
.options('port', {
default: '80',
describe: 'listen on this port for outside requests'
})
.argv;
if (argv.help) {
optimist.showHelp();
process.exit();
}
process.once('uncaughtException', function(err) {
log.panic(err);
process.exit(-1);
return;
});
var server = require('../server')({
max_tcp_sockets: 5
});
server.listen(argv.port, function() {
log.info('server listening on port: %d', server.address().port);
});
// vim: ft=javascript

337
client.js
View File

@@ -1,25 +1,136 @@
// builtin
var net = require('net'); var net = require('net');
var url = require('url'); var url = require('url');
var request = require('request');
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
// request upstream url and connection info var request = require('request');
var request_url = function(params, cb) { var debug = require('debug')('localtunnel:client');
request(params, function(err, res, body) {
if (err) {
cb(err);
}
cb(null, body); // 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);
}; };
var connect = function(opt) { TunnelCluster.prototype.__proto__ = EventEmitter.prototype;
var ev = new EventEmitter();
// local port // establish a new tunnel
var local_port = opt.port; 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);
if (remote.destroyed) {
self.emit('dead');
return;
}
remote.pause();
// connection to local http server
var local = net.connect({
host: local_host,
port: local_port
});
function remote_close() {
self.emit('dead');
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) {
debug('local connection closed [%s]', had_error);
});
});
}
// tunnel is considered open when remote connects
remote.once('connect', function() {
self.emit('open', remote);
});
remote.once('connect', conn_local);
};
var Tunnel = function(opt) {
if (!(this instanceof Tunnel)) {
return new Tunnel(opt);
}
var self = this;
self._closed = false;
self._opt = opt;
};
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 + '/'; var base_uri = opt.host + '/';
@@ -29,114 +140,108 @@ var connect = function(opt) {
// no subdomain at first, maybe use requested domain // no subdomain at first, maybe use requested domain
var assigned_domain = opt.subdomain; var assigned_domain = opt.subdomain;
// connect to upstream given connection parameters
var tunnel = function (remote_host, remote_port) {
var remote_opt = {
host: remote_host,
port: remote_port
};
var local_opt = {
host: 'localhost',
port: local_port
};
var remote_attempts = 0;
(function conn(conn_had_error) {
if (conn_had_error) {
return;
}
if (++remote_attempts >= 3) {
console.error('localtunnel server offline - try again');
process.exit(-1);
}
// connection to localtunnel server
var remote = net.connect(remote_opt);
remote.once('error', function(err) {
if (err.code !== 'ECONNREFUSED') {
local.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 // where to quest
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new'); params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
// get an id from lt server and setup forwarding tcp connections (function get_url() {
request_url(params, function(err, body) { 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 (err) { var port = body.port;
ev.emit('error', new Error('tunnel server not available: %s, retry 1s', err.message)); var host = upstream.hostname;
// retry interval for id request var max_conn = body.max_conn_count || 1;
return setTimeout(function() {
connect_proxy(opt);
}, 1000);
}
// our assigned hostname and tcp port cb(null, {
var port = body.port; remote_host: upstream.hostname,
var host = upstream.hostname; remote_port: body.port,
name: body.id,
// store the id so we can try to get the same one url: body.url,
assigned_domain = body.id; max_conn: max_conn
});
var max_conn = body.max_conn_count || 1; });
for (var count = 0 ; count < max_conn ; ++count) { })();
tunnel(host, port);
}
ev.emit('url', body.url);
});
return ev;
}; };
module.exports.connect = connect; Tunnel.prototype._establish = function(info) {
var self = this;
var opt = self._opt;
info.local_host = opt.local_host || 'localhost';
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() {
var self = this;
self._init(function(err, info) {
if (err) {
return self.emit('error', err);
}
self._establish(info);
});
};
// shutdown tunnels
Tunnel.prototype.close = function() {
var self = this;
self._closed = true;
self.emit('close');
};
module.exports.connect = function(opt) {
var client = Tunnel(opt);
client.open();
return client;
};

View File

@@ -1,2 +0,0 @@
module.exports.client = require('./client');
module.exports.server = require('./server');

View File

@@ -1,12 +0,0 @@
var chars = 'abcdefghiklmnopqrstuvwxyz';
module.exports = function rand_id() {
var randomstring = '';
for (var i=0; i<4; ++i) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars[rnum];
}
return randomstring;
}

View File

@@ -2,30 +2,19 @@
"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.0.4", "version": "0.2.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/shtylman/localtunnel.git" "url": "git://github.com/shtylman/localtunnel.git"
}, },
"dependencies": { "dependencies": {
"request": "2.11.4", "request": "2.11.4",
"book": "1.2.0",
"optimist": "0.3.4", "optimist": "0.3.4",
"http-raw": "1.1.0" "debug": "0.7.4"
},
"devDependencies": {
"mocha": "1.6.0"
},
"optionalDependencies": {},
"engines": {
"node": "*"
},
"scripts": {
"test": "mocha --ui qunit -- test",
"start": "./bin/server"
}, },
"devDependencies": {},
"bin": { "bin": {
"lt": "./bin/client" "lt": "./bin/client"
}, },
"main": "./index.js" "main": "./client.js"
} }

324
server.js
View File

@@ -1,324 +0,0 @@
// builtin
var http = require('http');
var net = require('net');
var url = require('url');
// here be dragons
var HTTPParser = process.binding('http_parser').HTTPParser;
// vendor
var log = require('book');
var createRawServer = require('http-raw');
// local
var rand_id = require('./lib/rand_id');
// id -> client http server
var clients = {};
// available parsers
var parsers = http.parsers;
// send this request to the appropriate client
// in -> incoming request stream
function proxy_request(client, req, res, rs, ws) {
rs = rs || req.createRawStream();
ws = ws || res.createRawStream();
// socket is a tcp connection back to the user hosting the site
var sock = client.sockets.shift();
// queue request
if (!sock) {
log.info('no more clients, queued: %s', req.url);
rs.pause();
client.waiting.push([req, res, rs, ws]);
return;
}
log.info('handle req: %s', req.url);
// pipe incoming request into tcp socket
// incoming request isn't allowed to end the socket back to lt client
rs.pipe(sock, { end: false });
sock.ws = ws;
sock.req = req;
// since tcp connection to upstream are kept open
// invoke parsing so we know when the response is complete
var parser = sock.parser;
parser.reinitialize(HTTPParser.RESPONSE);
parser.socket = sock;
// we have completed a response
// the tcp socket is free again
parser.onIncoming = function (res) {
parser.onMessageComplete = function() {
log.info('ended response: %s', req.url);
// any request we had going on is now done
ws.end();
// no more forwarding
delete sock.ws;
delete parser.onIncoming;
// return socket to available pool
client.sockets.push(sock);
var next = client.waiting.shift();
if (next) {
log.trace('popped');
proxy_request(client, next[0], next[1], next[2], next[3]);
}
};
};
rs.resume();
}
function upstream_response(d, start, end) {
var socket = this;
var ws = socket.ws;
if (!ws) {
log.warn('no stream set for req:', socket.req.url);
return;
}
ws.write(d.slice(start, end));
if (socket.upgraded) {
return;
}
var ret = socket.parser.execute(d, start, end - start);
if (ret instanceof Error) {
log.error(ret);
parsers.free(parser);
socket.destroy(ret);
}
}
var handle_req = function (req, res) {
var max_tcp_sockets = req.socket.server.max_tcp_sockets;
// ignore favicon
if (req.url === '/favicon.ico') {
res.writeHead(404);
return res.end();
}
var hostname = req.headers.host;
if (!hostname) {
log.trace('no hostname: %j', req.headers);
return res.end();
}
var match = hostname.match(/^([a-z]{4})[.].*/);
if (match) {
var client_id = match[1];
var client = clients[client_id];
// no such subdomain
if (!client) {
log.trace('no client found for id: ' + client_id);
res.statusCode = 404;
return res.end();
}
return proxy_request(client, req, res);
}
var parsed = url.parse(req.url, true);
// redirect main page to github reference
if (req.url === '/' && !parsed.query.new) {
res.writeHead(301, { Location: 'http://shtylman.github.com/localtunnel/' });
res.end();
return;
}
// at this point, the client is requesting a new tunnel setup
// either generate an id or use the one they requested
var match = req.url.match(/\/([a-z]{4})?/);
// user can request a particular set of characters
// will be given if not already taken
// this is useful when the main server is restarted
// users can keep testing with their expected ids
var requested_id;
if (match && match[1]) {
requested_id = match[1];
}
var id = requested_id || rand_id();
// if the id already exists, this client must use something else
if (clients[id]) {
id = rand_id();
}
// sockets is a list of available sockets for the connection
// waiting is?
var client = clients[id] = {
sockets: [],
waiting: []
};
var client_server = net.createServer();
client_server.listen(function() {
var port = client_server.address().port;
log.info('tcp server listening on port: %d', port);
var url = 'http://' + id + '.' + req.headers.host;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
url: url,
id: id,
port: port,
max_conn_count: max_tcp_sockets
}));
});
var conn_timeout;
// user has 5 seconds to connect before their slot is given up
function maybe_tcp_close() {
conn_timeout = setTimeout(client_server.close.bind(client_server), 5000);
}
maybe_tcp_close();
// no longer accepting connections for this id
client_server.on('close', function() {
log.trace('closed tcp socket for client(%s)', id);
clearTimeout(conn_timeout);
delete clients[id];
});
client_server.on('connection', function(socket) {
// no more socket connections allowed
if (client.sockets.length >= max_tcp_sockets) {
return socket.end();
}
log.trace('new connection for id: %s', id);
// no need to close the client server
clearTimeout(conn_timeout);
// allocate a response parser for the socket
// it only needs one since it will reuse it
socket.parser = parsers.alloc();
socket._orig_ondata = socket.ondata;
socket.ondata = upstream_response;
client.sockets.push(socket);
socket.once('close', function(had_error) {
log.trace('client %s closed socket', id);
// remove this socket
var idx = client.sockets.indexOf(socket);
client.sockets.splice(idx, 1);
log.trace('remaining client sockets: %s', client.sockets.length);
// no more sockets for this ident
if (client.sockets.length === 0) {
log.trace('all client(%s) sockets disconnected', id);
maybe_tcp_close();
}
});
// close will be emitted after this
socket.on('error', function(err) {
log.error(err);
socket.end();
});
});
client_server.on('error', function(err) {
log.error(err);
});
};
var handle_upgrade = function(req, ws) {
if (req.headers.connection !== 'Upgrade') {
return;
}
var hostname = req.headers.host;
if (!hostname) {
return res.end();
}
var match = hostname.match(/^([a-z]{4})[.].*/);
// not a valid client
if (!match) {
return res.end();
}
var client_id = match[1];
var client = clients[client_id];
if (!client) {
// no such subdomain
return res.end();
}
var socket = client.sockets.shift();
if (!socket) {
// no available sockets to upgrade to
return res.end();
}
var stream = req.createRawStream();
socket.ws = ws;
socket.upgraded = true;
stream.once('end', function() {
delete socket.ws;
// when this ends, we just reset the socket to the lt client
// this is easier than trying to figure anything else out
socket.end();
// put socket back into available pool
client.sockets.push(socket);
var next = client.waiting.shift();
if (next) {
log.trace('popped');
proxy_request(client, next[0], next[1], next[2], next[3]);
}
});
stream.pipe(socket, {end: false});
socket.once('end', ws.end.bind(ws));
};
module.exports = function(opt) {
opt = opt || {};
var server = createRawServer();
server.max_tcp_sockets = opt.max_tcp_sockets || 5;
server.on('request', handle_req);
server.on('upgrade', handle_upgrade);
return server;
};

View File

@@ -1,99 +0,0 @@
var http = require('http');
var url = require('url');
var assert = require('assert');
var localtunnel_server = require('../').server();
var localtunnel_client = require('../').client;
test('setup localtunnel server', function(done) {
localtunnel_server.listen(3000, function() {
console.log('lt server on:', 3000);
done();
});
});
test('setup local http server', function(done) {
var server = http.createServer();
server.on('request', function(req, res) {
res.write('foo');
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) {
var client = localtunnel_client.connect({
host: 'http://localhost:' + 3000,
port: test._fake_port
});
client.on('url', function(url) {
assert.ok(/^http:\/\/.*localhost:3000$/.test(url));
test._fake_url = url;
done();
});
client.on('error', function(err) {
console.error(err);
});
});
test('query localtunnel server w/ ident', function(done) {
var uri = test._fake_url;
var hostname = url.parse(uri).hostname;
var opt = {
host: 'localhost',
port: 3000,
headers: {
host: hostname
},
path: '/'
}
var req = http.request(opt, function(res) {
res.setEncoding('utf8');
var body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
assert.equal('foo', body);
// TODO(shtylman) shutdown client
done();
});
});
req.end();
});
test('request specific domain', function(done) {
var client = localtunnel_client.connect({
host: 'http://localhost:' + 3000,
port: test._fake_port,
subdomain: 'abcd'
});
client.on('url', function(url) {
assert.ok(/^http:\/\/abcd.localhost:3000$/.test(url));
done();
});
client.on('error', function(err) {
console.error(err);
});
});
test('shutdown', function() {
localtunnel_server.close();
});

View File

@@ -1,105 +0,0 @@
var http = require('http');
var url = require('url');
var assert = require('assert');
var localtunnel_server = require('../').server({
max_tcp_sockets: 1
});
var localtunnel_client = require('../').client;
var server;
test('setup localtunnel server', function(done) {
localtunnel_server.listen(3000, function() {
console.log('lt server on:', 3000);
done();
});
});
test('setup local http server', function(done) {
server = http.createServer();
server.on('request', function(req, res) {
// respond sometime later
setTimeout(function() {
res.setHeader('x-count', req.headers['x-count']);
res.end('foo');
}, 100);
});
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) {
var client = localtunnel_client.connect({
host: 'http://localhost:' + 3000,
port: test._fake_port
});
client.on('url', function(url) {
assert.ok(/^http:\/\/.*localhost:3000$/.test(url));
test._fake_url = url;
done();
});
client.on('error', function(err) {
console.error(err);
});
});
test('query localtunnel server w/ ident', function(done) {
var uri = test._fake_url;
var hostname = url.parse(uri).hostname;
var count = 0;
var opt = {
host: 'localhost',
port: 3000,
agent: false,
headers: {
host: hostname
},
path: '/'
}
var num_requests = 2;
var responses = 0;
function maybe_done() {
if (++responses >= num_requests) {
done();
}
}
function make_req() {
opt.headers['x-count'] = count++;
http.get(opt, function(res) {
res.setEncoding('utf8');
var body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
assert.equal('foo', body);
maybe_done();
});
});
}
for (var i=0 ; i<num_requests ; ++i) {
make_req();
}
});
test('shutdown', function() {
localtunnel_server.close();
});