mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-14 14:05:54 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,4 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 0.8
|
- 0.8
|
||||||
|
- 0.9
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -1,4 +1,4 @@
|
|||||||
# localtunnel [](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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
|
||||||
|
|
||||||
337
client.js
337
client.js
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
19
package.json
19
package.json
@@ -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
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
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