mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-13 21:45:54 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3026d6a42c | ||
|
|
abd461f83a | ||
|
|
2acea3d77f | ||
|
|
5d0eb3382a | ||
|
|
3b67c8a8ce | ||
|
|
71552a336e | ||
|
|
87a23bf28c | ||
|
|
3d54de851f | ||
|
|
92bb807908 | ||
|
|
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 | ||
|
|
5340659954 | ||
|
|
5c6558ed91 | ||
|
|
79ca069c38 | ||
|
|
741db27084 | ||
|
|
b605e9b823 | ||
|
|
b5830c3840 | ||
|
|
06b85ad0aa | ||
|
|
943a7dc35b | ||
|
|
c46a94b7a0 | ||
|
|
2f692b8e29 | ||
|
|
51d91ce0e8 | ||
|
|
ab28444802 |
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "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
|
||||
68
README.md
68
README.md
@@ -1,8 +1,8 @@
|
||||
# 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.
|
||||
|
||||
Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks!
|
||||
Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks.
|
||||
|
||||
## installation ##
|
||||
|
||||
@@ -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 East! 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
|
||||
@@ -21,3 +23,63 @@ 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.
|
||||
|
||||
### 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 (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 localtunnel = require('localtunnel');
|
||||
|
||||
localtunnel(port, function(err, tunnel) {
|
||||
if (err) ...
|
||||
|
||||
// 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
|
||||
|
||||
53
bin/client
Executable file
53
bin/client
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
var lt_client = require('../client');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
})
|
||||
.options('subdomain', {
|
||||
describe: 'request this subdomain'
|
||||
})
|
||||
.options('local-host', {
|
||||
describe: 'tunnel traffic to this host instead of localhost'
|
||||
})
|
||||
.options('version', {
|
||||
describe: 'print version and exit'
|
||||
})
|
||||
.default('local-host', 'localhost')
|
||||
.describe('port', 'internal http server port')
|
||||
.argv;
|
||||
|
||||
if (argv.version) {
|
||||
console.log(require('../package.json').version);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (argv.port == null) {
|
||||
require('optimist').showHelp();
|
||||
console.error('Missing required arguments: port');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var opt = {
|
||||
host: argv.host,
|
||||
port: argv.port,
|
||||
local_host: argv['local-host'],
|
||||
subdomain: argv.subdomain,
|
||||
};
|
||||
|
||||
lt_client(opt.port, opt, function(err, tunnel) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log('your url is: %s', tunnel.url);
|
||||
|
||||
tunnel.on('error', function(err) {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
// vim: ft=javascript
|
||||
349
client.js
349
client.js
@@ -1,95 +1,316 @@
|
||||
// builtin
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var request = require('request');
|
||||
var debug = require('debug')('localtunnel:client');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.demand(['port'])
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
})
|
||||
.describe('port', 'internal http server port')
|
||||
.argv;
|
||||
var stream = require('stream');
|
||||
var util = require('util');
|
||||
|
||||
// local port
|
||||
var local_port = argv.port;
|
||||
var Transform = stream.Transform;
|
||||
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(argv.host);
|
||||
var HeaderHostTransformer = function(opts) {
|
||||
if (!(this instanceof HeaderHostTransformer)) {
|
||||
return new HeaderHostTransformer(opts);
|
||||
}
|
||||
|
||||
// query options
|
||||
var opt = {
|
||||
host: upstream.hostname,
|
||||
port: upstream.port || 80,
|
||||
path: '/',
|
||||
json: true
|
||||
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();
|
||||
};
|
||||
|
||||
var base_uri = 'http://' + opt.host + ':' + opt.port + opt.path;
|
||||
// manages groups of tunnels
|
||||
var TunnelCluster = function(opt) {
|
||||
if (!(this instanceof TunnelCluster)) {
|
||||
return new TunnelCluster(opt);
|
||||
}
|
||||
|
||||
var internal;
|
||||
var upstream;
|
||||
var prev_id;
|
||||
var self = this;
|
||||
self._opt = opt;
|
||||
|
||||
(function connect_proxy() {
|
||||
opt.uri = base_uri + ((prev_id) ? prev_id : '?new');
|
||||
EventEmitter.call(self);
|
||||
};
|
||||
|
||||
request(opt, function(err, res, body) {
|
||||
if (err) {
|
||||
console.error('upstream not available: %s', err.message);
|
||||
return process.exit(-1);
|
||||
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);
|
||||
}
|
||||
|
||||
// our assigned hostname and tcp port
|
||||
var port = body.port;
|
||||
var host = opt.host;
|
||||
setTimeout(function() {
|
||||
self.emit('dead');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// store the id so we can try to get the same one
|
||||
prev_id = body.id;
|
||||
function conn_local() {
|
||||
if (remote.destroyed) {
|
||||
debug('remote destroyed');
|
||||
self.emit('dead');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('your url is: %s', body.url);
|
||||
debug('connecting locally to %s:%d', local_host, local_port);
|
||||
remote.pause();
|
||||
|
||||
// connect to remote tcp server
|
||||
upstream = net.createConnection(port, host);
|
||||
// connection to local http server
|
||||
var local = net.connect({
|
||||
host: local_host,
|
||||
port: local_port
|
||||
});
|
||||
|
||||
// reconnect internal
|
||||
connect_internal();
|
||||
function remote_close() {
|
||||
debug('remote close');
|
||||
self.emit('dead');
|
||||
local.end();
|
||||
};
|
||||
|
||||
upstream.on('end', function() {
|
||||
console.log('> upstream connection terminated');
|
||||
remote.once('close', remote_close);
|
||||
|
||||
// sever connection to internal server
|
||||
// on reconnect we will re-establish
|
||||
internal.end();
|
||||
// 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();
|
||||
|
||||
setTimeout(function() {
|
||||
connect_proxy();
|
||||
}, 1000);
|
||||
remote.removeListener('close', remote_close);
|
||||
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return remote.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 Tunnel = function(opt) {
|
||||
if (!(this instanceof Tunnel)) {
|
||||
return new Tunnel(opt);
|
||||
}
|
||||
|
||||
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 + '/';
|
||||
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(opt.host);
|
||||
|
||||
// no subdomain at first, maybe use requested domain
|
||||
var assigned_domain = opt.subdomain;
|
||||
|
||||
// where to quest
|
||||
params.uri = base_uri + ((assigned_domain) ? assigned_domain : '?new');
|
||||
|
||||
(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 (res.statusCode !== 200) {
|
||||
var err = new Error((body && body.message) || 'localtunnel server returned an error, please try again');
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var port = body.port;
|
||||
var host = upstream.hostname;
|
||||
|
||||
var max_conn = body.max_conn_count || 1;
|
||||
|
||||
cb(null, {
|
||||
remote_host: upstream.hostname,
|
||||
remote_port: body.port,
|
||||
name: body.id,
|
||||
url: body.url,
|
||||
max_conn: max_conn
|
||||
});
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function connect_internal() {
|
||||
// when a tunnel dies, open a new one
|
||||
tunnels.on('dead', function(tunnel) {
|
||||
tunnel_count--;
|
||||
debug('tunnel dead [total: %d]', tunnel_count);
|
||||
|
||||
internal = net.createConnection(local_port);
|
||||
internal.on('error', function(err) {
|
||||
console.log('error connecting to local server. retrying in 1s');
|
||||
if (self._closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
tunnels.open();
|
||||
});
|
||||
|
||||
internal.on('end', function() {
|
||||
console.log('disconnected from local server. retrying in 1s');
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
// 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();
|
||||
});
|
||||
};
|
||||
|
||||
upstream.pipe(internal);
|
||||
internal.pipe(upstream);
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
20
package.json
20
package.json
@@ -2,22 +2,24 @@
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"name": "localtunnel",
|
||||
"description": "expose localhost to the world",
|
||||
"version": "0.0.1",
|
||||
"version": "1.2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/shtylman/localtunnel.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "2.11.4",
|
||||
"book": "1.2.0",
|
||||
"optimist": "0.3.4"
|
||||
"optimist": "0.3.4",
|
||||
"debug": "0.7.4"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"devDependencies": {
|
||||
"mocha": "~1.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --ui qunit --reporter list --timeout 10000 -- test/index.js"
|
||||
},
|
||||
"bin": {
|
||||
"lt": "./bin/lt"
|
||||
}
|
||||
"lt": "./bin/client"
|
||||
},
|
||||
"main": "./client.js"
|
||||
}
|
||||
|
||||
343
server.js
343
server.js
@@ -1,343 +0,0 @@
|
||||
|
||||
// builtin
|
||||
var http = require('http');
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var FreeList = require('freelist').FreeList;
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.options('port', {
|
||||
default: '80',
|
||||
describe: 'listen on this port for outside requests'
|
||||
})
|
||||
.argv;
|
||||
|
||||
if (argv.help) {
|
||||
require('optimist').showHelp();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// here be dragons
|
||||
var HTTPParser = process.binding('http_parser').HTTPParser;
|
||||
var ServerResponse = http.ServerResponse;
|
||||
var IncomingMessage = http.IncomingMessage;
|
||||
|
||||
var log = require('book');
|
||||
|
||||
var chars = 'abcdefghiklmnopqrstuvwxyz';
|
||||
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;
|
||||
}
|
||||
|
||||
var server = http.createServer();
|
||||
|
||||
// id -> client http server
|
||||
var clients = {};
|
||||
|
||||
// id -> list of sockets waiting for a valid response
|
||||
var wait_list = {};
|
||||
|
||||
var parsers = http.parsers;
|
||||
|
||||
// data going back to a client (the last client that made a request)
|
||||
function socketOnData(d, start, end) {
|
||||
|
||||
var socket = this;
|
||||
var req = this._httpMessage;
|
||||
|
||||
var current = clients[socket.subdomain].current;
|
||||
|
||||
if (!current) {
|
||||
log.error('no current for http response from backend');
|
||||
return;
|
||||
}
|
||||
|
||||
// send the goodies
|
||||
current.write(d.slice(start, end));
|
||||
|
||||
// invoke parsing so we know when all the goodies have been sent
|
||||
var parser = current.out_parser;
|
||||
parser.socket = socket;
|
||||
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
if (ret instanceof Error) {
|
||||
debug('parse error');
|
||||
freeParser(parser, req);
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function freeParser(parser, req) {
|
||||
if (parser) {
|
||||
parser._headers = [];
|
||||
parser.onIncoming = null;
|
||||
if (parser.socket) {
|
||||
parser.socket.onend = null;
|
||||
parser.socket.ondata = null;
|
||||
parser.socket.parser = null;
|
||||
}
|
||||
parser.socket = null;
|
||||
parser.incoming = null;
|
||||
parsers.free(parser);
|
||||
parser = null;
|
||||
}
|
||||
if (req) {
|
||||
req.parser = null;
|
||||
}
|
||||
}
|
||||
|
||||
// single http connection
|
||||
// gets a single http response back
|
||||
server.on('connection', function(socket) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var for_client = false;
|
||||
var client_id;
|
||||
|
||||
var request;
|
||||
|
||||
var parser = parsers.alloc();
|
||||
parser.socket = socket;
|
||||
parser.reinitialize(HTTPParser.REQUEST);
|
||||
|
||||
// a full request is complete
|
||||
// we wait for the response from the server
|
||||
parser.onIncoming = function(req, shouldKeepAlive) {
|
||||
|
||||
log.trace('request', req.url);
|
||||
request = req;
|
||||
|
||||
for_client = false;
|
||||
|
||||
var hostname = req.headers.host;
|
||||
|
||||
if (!hostname) {
|
||||
log.trace('no hostname: %j', req.headers);
|
||||
// normal processing if not proxy
|
||||
var res = new ServerResponse(req);
|
||||
|
||||
// TODO(shtylman) skip favicon for now, it caused problems
|
||||
if (req.url === '/favicon.ico') {
|
||||
return;
|
||||
}
|
||||
|
||||
res.assignSocket(parser.socket);
|
||||
self.emit('request', req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
var match = hostname.match(/^([a-z]{4})[.].*/);
|
||||
|
||||
if (!match) {
|
||||
// normal processing if not proxy
|
||||
var res = new ServerResponse(req);
|
||||
|
||||
// TODO(shtylman) skip favicon for now, it caused problems
|
||||
if (req.url === '/favicon.ico') {
|
||||
return;
|
||||
}
|
||||
|
||||
res.assignSocket(parser.socket);
|
||||
self.emit('request', req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
client_id = match[1];
|
||||
for_client = true;
|
||||
|
||||
var out_parser = parsers.alloc();
|
||||
out_parser.reinitialize(HTTPParser.RESPONSE);
|
||||
socket.out_parser = out_parser;
|
||||
|
||||
// we have a response
|
||||
out_parser.onIncoming = function(res) {
|
||||
res.on('end', function() {
|
||||
log.trace('done with response for: %s', req.url);
|
||||
|
||||
// done with the parser
|
||||
parsers.free(out_parser);
|
||||
|
||||
var next = wait_list[client_id].shift();
|
||||
|
||||
clients[client_id].current = next;
|
||||
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
// write original bytes that we held cause client was busy
|
||||
clients[client_id].write(next.queue);
|
||||
next.resume();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// process new data on the client socket
|
||||
// we may need to forward this it the backend
|
||||
socket.ondata = function(d, start, end) {
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
|
||||
// invalid request from the user
|
||||
if (ret instanceof Error) {
|
||||
debug('parse error');
|
||||
socket.destroy(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
// only write data if previous request to this client is done?
|
||||
log.trace('%s %s', parser.incoming && parser.incoming.upgrade, for_client);
|
||||
|
||||
// what if the subdomains are treated differently
|
||||
// as individual channels to the backend if available?
|
||||
// how can I do that?
|
||||
|
||||
if (parser.incoming && parser.incoming.upgrade) {
|
||||
// websocket shit
|
||||
}
|
||||
|
||||
// wtf do you do with upgraded connections?
|
||||
|
||||
// forward the data to the backend
|
||||
if (for_client) {
|
||||
var client = clients[client_id];
|
||||
|
||||
// requesting a subdomain that doesn't exist
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the client is already processing something
|
||||
// then new connections need to go into pause mode
|
||||
// and when they are revived, then they can send data along
|
||||
if (client.current && client.current !== socket) {
|
||||
log.trace('pausing', request.url);
|
||||
// prevent new data from gathering for this connection
|
||||
// we are waiting for a response to a previous request
|
||||
socket.pause();
|
||||
|
||||
var copy = Buffer(end - start);
|
||||
d.copy(copy, 0, start, end);
|
||||
socket.queue = copy;
|
||||
|
||||
wait_list[client_id].push(socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// this socket needs to receive responses
|
||||
client.current = socket;
|
||||
|
||||
// send through tcp tunnel
|
||||
client.write(d.slice(start, end));
|
||||
}
|
||||
};
|
||||
|
||||
socket.onend = function() {
|
||||
var ret = parser.finish();
|
||||
|
||||
if (ret instanceof Error) {
|
||||
log.trace('parse error');
|
||||
socket.destroy(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.end();
|
||||
};
|
||||
|
||||
socket.on('close', function() {
|
||||
parsers.free(parser);
|
||||
});
|
||||
});
|
||||
|
||||
server.on('request', function(req, res) {
|
||||
|
||||
// ignore favicon
|
||||
if (req.url === '/favicon.ico') {
|
||||
res.writeHead(404);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 (wait_list[id]) {
|
||||
// new id
|
||||
id = rand_id();
|
||||
}
|
||||
|
||||
// generate new shit for client
|
||||
if (wait_list[id]) {
|
||||
wait_list[id].forEach(function(waiting) {
|
||||
waiting.end();
|
||||
});
|
||||
}
|
||||
|
||||
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 }));
|
||||
});
|
||||
|
||||
// user has 5 seconds to connect before their slot is given up
|
||||
var conn_timeout = setTimeout(function() {
|
||||
client_server.close();
|
||||
}, 5000);
|
||||
|
||||
client_server.on('connection', function(socket) {
|
||||
|
||||
// who the info should route back to
|
||||
socket.subdomain = id;
|
||||
|
||||
// multiplexes socket data out to clients
|
||||
socket.ondata = socketOnData;
|
||||
|
||||
clearTimeout(conn_timeout);
|
||||
|
||||
log.trace('new connection for id: %s', id);
|
||||
clients[id] = socket;
|
||||
wait_list[id] = [];
|
||||
|
||||
socket.on('end', function() {
|
||||
delete clients[id];
|
||||
});
|
||||
});
|
||||
|
||||
client_server.on('err', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(argv.port, function() {
|
||||
log.info('server listening on port: %d', server.address().port);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user