expose client as a library

- Allows for using localtunnel from code instead of manually invoking
- add tests
- add travis config
- add travis badge
This commit is contained in:
Roman Shtylman
2012-11-03 15:13:36 -04:00
parent 51d91ce0e8
commit 2f692b8e29
6 changed files with 242 additions and 69 deletions

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- 0.8

View File

@@ -1,4 +1,4 @@
# localtunnel #
# localtunnel [![Build Status](https://secure.travis-ci.org/shtylman/localtunnel.png)](http://travis-ci.org/shtylman/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.
@@ -21,3 +21,28 @@ lt --port 8000
Thats it! It will connect to the tunnel server, setup the tunnel, and tell you what url to use for your testing. This url will remain active for the duration of your session; so feel free to share it with others for happy fun time!
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
## API ##
The localtunnel client is also usable through an API (test integration, automation, etc)
```javascript
var lt_client = require('localtunnel').client;
var client = lt_client.connect({
// the localtunnel server
host: 'http://localtunnel.com',
// your local application port
port: 12345
});
// when your are assigned a url
client.on('url', function(url) {
// you can now make http requests to the url
// they will be proxied to your local server on port [12345]
});
client.on('error', function(err) {
// uh oh!
});
```

View File

@@ -1,4 +1,34 @@
#!/usr/bin/env node
require(__dirname + '/../client');
var lt_client = require(__dirname + '/../client');
var argv = require('optimist')
.usage('Usage: $0 --port [num]')
.demand(['port'])
.options('host', {
default: 'http://localtunnel.me',
describe: 'upstream server providing forwarding'
})
.options('subdomain', {
describe: 'request this subdomain'
})
.describe('port', 'internal http server port')
.argv;
var opt = {
host: argv.host,
port: argv.port,
subdomain: argv.subdomain,
}
var client = lt_client.connect(opt);
// only emitted when the url changes
client.on('url', function(url) {
console.log('your url is: %s', url);
});
client.on('error', function(err) {
console.error(err);
});
// vim: ft=javascript

146
client.js
View File

@@ -2,118 +2,132 @@
var net = require('net');
var url = require('url');
var request = require('request');
var EventEmitter = require('events').EventEmitter;
var argv = require('optimist')
.usage('Usage: $0 --port [num]')
.demand(['port'])
.options('host', {
default: 'http://localtunnel.me',
describe: 'upstream server providing forwarding'
})
.options('subdomain', {
describe: 'request this subdomain'
})
.describe('port', 'internal http server port')
.argv;
// local port
var local_port = argv.port;
// optionally override the upstream server
var upstream = url.parse(argv.host);
// query options
var opt = {
host: upstream.hostname,
port: upstream.port || 80,
path: '/',
json: true
};
var base_uri = 'http://' + opt.host + ':' + opt.port + opt.path;
var prev_id = argv.subdomain || '';
(function connect_proxy() {
opt.uri = base_uri + ((prev_id) ? prev_id : '?new');
request(opt, function(err, res, body) {
// request upstream url and connection info
var request_url = function(params, cb) {
request(params, function(err, res, body) {
if (err) {
console.error('tunnel server not available: %s, retry 1s', err.message);
// retry interval for id request
return setTimeout(function() {
connect_proxy();
}, 1000);
cb(err);
}
// our assigned hostname and tcp port
var port = body.port;
var host = opt.host;
var max_conn = body.max_conn_count || 1;
cb(null, body);
});
};
// store the id so we can try to get the same one
prev_id = body.id;
var connect = function(opt) {
var ev = new EventEmitter();
console.log('your url is: %s', body.url);
// local port
var local_port = opt.port;
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;
// connect to upstream given connection parameters
var tunnel = function (remote_host, remote_port, max_conn) {
var count = 0;
// open 5 connections to the localtunnel server
// allows for resources to be served faster
for (var count = 0 ; count < max_conn ; ++count) {
var upstream = duplex(port, host, local_port, 'localhost');
var upstream = duplex(remote_host, remote_port, 'localhost', local_port);
upstream.once('end', function() {
// all upstream connections have been closed
if (--count <= 0) {
connect_proxy();
tunnel(remote_host, remote_port, max_conn);
}
});
}
});
})();
function duplex(port, host, local_port, local_host) {
upstream.on('error', function(err) {
console.error(err);
});
}
};
var params = {
path: '/',
json: true
};
// where to quest
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
request_url(params, function(err, body) {
if (err) {
ev.emit('error', new Error('tunnel server not available: %s, retry 1s', err.message));
// retry interval for id request
return setTimeout(function() {
connect_proxy(opt);
}, 1000);
}
// our assigned hostname and tcp port
var port = body.port;
var host = upstream.hostname;
// store the id so we can try to get the same one
assigned_domain = body.id;
tunnel(host, port, body.max_conn_count || 1);
ev.emit('url', body.url);
});
return ev;
};
var duplex = function(remote_host, remote_port, local_host, local_port) {
var ev = new EventEmitter();
// connect to remote tcp server
var upstream = net.createConnection(port, host);
var internal = net.createConnection(local_port, local_host);
var upstream = net.createConnection(remote_port, remote_host);
var internal;
// when upstream connection is closed, close other associated connections
upstream.on('end', function() {
console.log('> upstream connection terminated');
upstream.once('end', function() {
ev.emit('error', new Error('upstream connection terminated'));
// sever connection to internal server
// on reconnect we will re-establish
internal.end();
ev.emit('end');
});
upstream.on('error', function(err) {
console.error(err);
ev.emit('error', err);
});
(function connect_internal() {
//internal = net.createConnection(local_port);
internal.on('error', function(err) {
console.log('error connecting to local server. retrying in 1s');
internal = net.createConnection(local_port, local_host);
internal.on('error', function() {
ev.emit('error', new Error('error connecting to local server. retrying in 1s'));
setTimeout(function() {
connect_internal();
}, 1000);
});
internal.on('end', function() {
console.log('disconnected from local server. retrying in 1s');
ev.emit('error', new Error('disconnected from local server. retrying in 1s'));
setTimeout(function() {
connect_internal();
}, 1000);
});
upstream.pipe(internal);
internal.pipe(upstream);
upstream.pipe(internal).pipe(upstream);
})();
return upstream;
return ev;
}
module.exports.connect = connect;

View File

@@ -12,11 +12,17 @@
"book": "1.2.0",
"optimist": "0.3.4"
},
"devDependencies": {},
"devDependencies": {
"mocha": "1.6.0"
},
"optionalDependencies": {},
"engines": {
"node": "*"
},
"scripts": {
"test": "mocha --ui qunit -- test",
"start": "./bin/server"
},
"bin": {
"lt": "./bin/client"
}

95
test.js Normal file
View File

@@ -0,0 +1,95 @@
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);
});
});