mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-14 05:55:53 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbdc3697e | ||
|
|
0049f21b55 | ||
|
|
509841104b | ||
|
|
92caf2f204 | ||
|
|
9487797e02 | ||
|
|
a42f6a8d8d | ||
|
|
14b4bcb96f | ||
|
|
4aa65002eb | ||
|
|
08676ba81d | ||
|
|
174e7f3982 | ||
|
|
44be55cd7b | ||
|
|
5c6cd2359c | ||
|
|
2f6f9459ad | ||
|
|
7217a08a05 | ||
|
|
fbfc923a7e | ||
|
|
d9bc11b520 | ||
|
|
ad64611bd1 | ||
|
|
ac70515143 | ||
|
|
8d7ccccf21 | ||
|
|
77091b3d93 | ||
|
|
4f4a147b45 | ||
|
|
f1d809a84d | ||
|
|
eba003bd26 | ||
|
|
3354c4c6e3 | ||
|
|
1c2757e604 | ||
|
|
790e55e881 | ||
|
|
a9b0274ff4 | ||
|
|
83ecb29eff | ||
|
|
21df257d16 | ||
|
|
18ada0854a | ||
|
|
34afd6537d | ||
|
|
2c38aefb9d | ||
|
|
aa488f6e76 | ||
|
|
f6618953f9 | ||
|
|
092d050fa0 | ||
|
|
0334ace20b | ||
|
|
13afcff1ae | ||
|
|
ed5aa3f16b | ||
|
|
2fcac1336c | ||
|
|
0568ae0bef | ||
|
|
585a8afad7 | ||
|
|
fbe841a1c5 | ||
|
|
929473913f |
@@ -1,3 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 0.8
|
||||
- "0.10"
|
||||
|
||||
7
History.md
Normal file
7
History.md
Normal 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
|
||||
73
README.md
73
README.md
@@ -1,4 +1,4 @@
|
||||
# localtunnel [](http://travis-ci.org/shtylman/localtunnel) #
|
||||
# localtunnel [](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.
|
||||
|
||||
@@ -10,9 +10,11 @@ Great for working with browser testing tools like browserling or external api ca
|
||||
npm install -g localtunnel
|
||||
```
|
||||
|
||||
This will install the localtunnel module globally and add the 'lt' client cli tool to your PATH.
|
||||
|
||||
## 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
|
||||
@@ -22,27 +24,62 @@ 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 (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
|
||||
var lt_client = require('localtunnel').client;
|
||||
var localtunnel = require('localtunnel');
|
||||
|
||||
var client = lt_client.connect({
|
||||
// the localtunnel server
|
||||
host: 'http://localtunnel.me',
|
||||
// your local application port
|
||||
port: 12345
|
||||
});
|
||||
localtunnel(port, function(err, tunnel) {
|
||||
if (err) ...
|
||||
|
||||
// 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!
|
||||
// the assigned public url for your tunnel
|
||||
// i.e. https://abcdefgjhij.localtunnel.me
|
||||
tunnel.url;
|
||||
});
|
||||
```
|
||||
|
||||
### 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 ##
|
||||
|
||||
Clients in other languages
|
||||
|
||||
*go* [gotunnelme](https://github.com/NoahShen/gotunnelme)
|
||||
|
||||
## server ##
|
||||
|
||||
See defunctzombie/localtunnel-server for details on the server that powers localtunnel.
|
||||
|
||||
## License ##
|
||||
MIT
|
||||
|
||||
22
bin/client
22
bin/client
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
var lt_client = require(__dirname + '/../client');
|
||||
var lt_client = require('../client');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
@@ -11,24 +11,30 @@ var argv = require('optimist')
|
||||
.options('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')
|
||||
.argv;
|
||||
|
||||
var opt = {
|
||||
host: argv.host,
|
||||
port: argv.port,
|
||||
local_host: argv['local-host'],
|
||||
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
|
||||
client.on('url', function(url) {
|
||||
console.log('your url is: %s', url);
|
||||
});
|
||||
console.log('your url is: %s', tunnel.url);
|
||||
|
||||
client.on('error', function(err) {
|
||||
console.error(err);
|
||||
tunnel.on('error', function(err) {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
// vim: ft=javascript
|
||||
|
||||
35
bin/server
35
bin/server
@@ -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
|
||||
|
||||
396
client.js
396
client.js
@@ -1,25 +1,184 @@
|
||||
// builtin
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var request = require('request');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
// request upstream url and connection info
|
||||
var request_url = function(params, cb) {
|
||||
request(params, function(err, res, body) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
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
|
||||
self.push(chunk.replace(/(\r\nHost: )\S+/, function(match, $1) {
|
||||
self._transform = undefined;
|
||||
return $1 + self.host;
|
||||
}));
|
||||
cb();
|
||||
};
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
TunnelCluster.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
// establish a new tunnel
|
||||
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);
|
||||
}
|
||||
|
||||
cb(null, body);
|
||||
setTimeout(function() {
|
||||
self.emit('dead');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
function conn_local() {
|
||||
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
|
||||
var local = net.connect({
|
||||
host: local_host,
|
||||
port: local_port
|
||||
});
|
||||
|
||||
function remote_close() {
|
||||
debug('remote close');
|
||||
self.emit('dead');
|
||||
local.end();
|
||||
};
|
||||
|
||||
remote.once('close', remote_close);
|
||||
|
||||
// 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 remove.end();
|
||||
}
|
||||
|
||||
// retrying connection to local server
|
||||
setTimeout(conn_local, 1000);
|
||||
});
|
||||
|
||||
local.once('connect', function() {
|
||||
debug('connected locally');
|
||||
remote.resume();
|
||||
|
||||
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
|
||||
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);
|
||||
conn_local();
|
||||
});
|
||||
};
|
||||
|
||||
var connect = function(opt) {
|
||||
var ev = new EventEmitter();
|
||||
var Tunnel = function(opt) {
|
||||
if (!(this instanceof Tunnel)) {
|
||||
return new Tunnel(opt);
|
||||
}
|
||||
|
||||
// local port
|
||||
var local_port = opt.port;
|
||||
var self = this;
|
||||
self._closed = false;
|
||||
self._opt = opt || {};
|
||||
|
||||
self._opt.host = self._opt.host || 'https://localtunnel.me';
|
||||
};
|
||||
|
||||
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 + '/';
|
||||
|
||||
@@ -29,114 +188,123 @@ var connect = function(opt) {
|
||||
// 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) {
|
||||
|
||||
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
|
||||
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
||||
|
||||
// get an id from lt server and setup forwarding tcp connections
|
||||
request_url(params, function(err, body) {
|
||||
(function get_url() {
|
||||
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) {
|
||||
ev.emit('error', new Error('tunnel server not available: %s, retry 1s', err.message));
|
||||
var port = body.port;
|
||||
var host = upstream.hostname;
|
||||
|
||||
// retry interval for id request
|
||||
return setTimeout(function() {
|
||||
connect_proxy(opt);
|
||||
}, 1000);
|
||||
}
|
||||
var max_conn = body.max_conn_count || 1;
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
cb(null, {
|
||||
remote_host: upstream.hostname,
|
||||
remote_port: body.port,
|
||||
name: body.id,
|
||||
url: body.url,
|
||||
max_conn: max_conn
|
||||
});
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
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(cb) {
|
||||
var self = this;
|
||||
|
||||
self._init(function(err, info) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
self.url = info.url;
|
||||
self._establish(info);
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
// shutdown tunnels
|
||||
Tunnel.prototype.close = function() {
|
||||
var self = this;
|
||||
|
||||
self._closed = true;
|
||||
self.emit('close');
|
||||
};
|
||||
|
||||
module.exports = function localtunnel(port, opt, fn) {
|
||||
if (typeof opt === 'function') {
|
||||
fn = opt;
|
||||
opt = {};
|
||||
}
|
||||
|
||||
opt = opt || {};
|
||||
opt.port = port;
|
||||
|
||||
var client = Tunnel(opt);
|
||||
client.open(function(err) {
|
||||
if (err) {
|
||||
return fn(err);
|
||||
}
|
||||
|
||||
fn(null, client);
|
||||
});
|
||||
};
|
||||
|
||||
2
index.js
2
index.js
@@ -1,2 +0,0 @@
|
||||
module.exports.client = require('./client');
|
||||
module.exports.server = require('./server');
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
16
package.json
16
package.json
@@ -2,30 +2,24 @@
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"name": "localtunnel",
|
||||
"description": "expose localhost to the world",
|
||||
"version": "0.0.4",
|
||||
"version": "1.1.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/shtylman/localtunnel.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "2.11.4",
|
||||
"book": "1.2.0",
|
||||
"optimist": "0.3.4",
|
||||
"http-raw": "1.1.0"
|
||||
"debug": "0.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.6.0"
|
||||
},
|
||||
"optionalDependencies": {},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"mocha": "~1.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --ui qunit -- test",
|
||||
"start": "./bin/server"
|
||||
"test": "mocha --ui qunit --reporter list -- test/index.js"
|
||||
},
|
||||
"bin": {
|
||||
"lt": "./bin/client"
|
||||
},
|
||||
"main": "./index.js"
|
||||
"main": "./client.js"
|
||||
}
|
||||
|
||||
324
server.js
324
server.js
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
113
test/index.js
Normal file
113
test/index.js
Normal 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();
|
||||
});
|
||||
105
test/queue.js
105
test/queue.js
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user