mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-13 13:35:54 +00:00
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:
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 0.8
|
||||
27
README.md
27
README.md
@@ -1,4 +1,4 @@
|
||||
# localtunnel #
|
||||
# localtunnel [](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!
|
||||
});
|
||||
```
|
||||
|
||||
32
bin/client
32
bin/client
@@ -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
146
client.js
@@ -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;
|
||||
|
||||
|
||||
@@ -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
95
test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user