mirror of
https://github.com/bitinflow/localtunnel.git
synced 2026-03-14 05:55:53 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.8
|
||||
31
README.md
31
README.md
@@ -1,8 +1,8 @@
|
||||
# localtunnel #
|
||||
# localtunnel [](http://travis-ci.org/shtylman/localtunnel) #
|
||||
|
||||
localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes.
|
||||
|
||||
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 ##
|
||||
|
||||
@@ -12,7 +12,7 @@ npm install -g localtunnel
|
||||
|
||||
## use ##
|
||||
|
||||
Super East! 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.
|
||||
|
||||
```
|
||||
lt --port 8000
|
||||
@@ -21,3 +21,28 @@ lt --port 8000
|
||||
Thats it! It will connect to the tunnel server, setup the tunnel, and tell you what url to use for your testing. This url will remain active for the duration of your session; so feel free to share it with others for happy fun time!
|
||||
|
||||
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
|
||||
|
||||
## API ##
|
||||
|
||||
The localtunnel client is also usable through an API (test integration, automation, etc)
|
||||
|
||||
```javascript
|
||||
var lt_client = require('localtunnel').client;
|
||||
|
||||
var client = lt_client.connect({
|
||||
// the localtunnel server
|
||||
host: 'http://localtunnel.me',
|
||||
// your local application port
|
||||
port: 12345
|
||||
});
|
||||
|
||||
// when your are assigned a url
|
||||
client.on('url', function(url) {
|
||||
// you can now make http requests to the url
|
||||
// they will be proxied to your local server on port [12345]
|
||||
});
|
||||
|
||||
client.on('error', function(err) {
|
||||
// uh oh!
|
||||
});
|
||||
```
|
||||
|
||||
34
bin/client
Executable file
34
bin/client
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
var lt_client = require(__dirname + '/../client');
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.demand(['port'])
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
})
|
||||
.options('subdomain', {
|
||||
describe: 'request this subdomain'
|
||||
})
|
||||
.describe('port', 'internal http server port')
|
||||
.argv;
|
||||
|
||||
var opt = {
|
||||
host: argv.host,
|
||||
port: argv.port,
|
||||
subdomain: argv.subdomain,
|
||||
}
|
||||
|
||||
var client = lt_client.connect(opt);
|
||||
|
||||
// only emitted when the url changes
|
||||
client.on('url', function(url) {
|
||||
console.log('your url is: %s', url);
|
||||
});
|
||||
|
||||
client.on('error', function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// vim: ft=javascript
|
||||
35
bin/server
Executable file
35
bin/server
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/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
|
||||
|
||||
189
client.js
189
client.js
@@ -2,94 +2,141 @@
|
||||
var net = require('net');
|
||||
var url = require('url');
|
||||
var request = require('request');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var argv = require('optimist')
|
||||
.usage('Usage: $0 --port [num]')
|
||||
.demand(['port'])
|
||||
.options('host', {
|
||||
default: 'http://localtunnel.me',
|
||||
describe: 'upstream server providing forwarding'
|
||||
})
|
||||
.describe('port', 'internal http server port')
|
||||
.argv;
|
||||
// request upstream url and connection info
|
||||
var request_url = function(params, cb) {
|
||||
request(params, function(err, res, body) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
}
|
||||
|
||||
// local port
|
||||
var local_port = argv.port;
|
||||
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(argv.host);
|
||||
|
||||
// query options
|
||||
var opt = {
|
||||
host: upstream.hostname,
|
||||
port: upstream.port || 80,
|
||||
path: '/',
|
||||
json: true
|
||||
cb(null, body);
|
||||
});
|
||||
};
|
||||
|
||||
var base_uri = 'http://' + opt.host + ':' + opt.port + opt.path;
|
||||
var connect = function(opt) {
|
||||
var ev = new EventEmitter();
|
||||
|
||||
var internal;
|
||||
var upstream;
|
||||
var prev_id;
|
||||
// local port
|
||||
var local_port = opt.port;
|
||||
|
||||
(function connect_proxy() {
|
||||
opt.uri = base_uri + ((prev_id) ? prev_id : '?new');
|
||||
var base_uri = opt.host + '/';
|
||||
|
||||
// optionally override the upstream server
|
||||
var upstream = url.parse(opt.host);
|
||||
|
||||
// no subdomain at first, maybe use requested domain
|
||||
var assigned_domain = opt.subdomain;
|
||||
|
||||
// connect to upstream given connection parameters
|
||||
var tunnel = function (remote_host, remote_port) {
|
||||
|
||||
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) {
|
||||
|
||||
request(opt, function(err, res, body) {
|
||||
if (err) {
|
||||
console.error('upstream not available: %s', err.message);
|
||||
return process.exit(-1);
|
||||
ev.emit('error', new Error('tunnel server not available: %s, retry 1s', err.message));
|
||||
|
||||
// retry interval for id request
|
||||
return setTimeout(function() {
|
||||
connect_proxy(opt);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// our assigned hostname and tcp port
|
||||
var port = body.port;
|
||||
var host = opt.host;
|
||||
var host = upstream.hostname;
|
||||
|
||||
// store the id so we can try to get the same one
|
||||
prev_id = body.id;
|
||||
assigned_domain = body.id;
|
||||
|
||||
console.log('your url is: %s', body.url);
|
||||
var max_conn = body.max_conn_count || 1;
|
||||
for (var count = 0 ; count < max_conn ; ++count) {
|
||||
tunnel(host, port);
|
||||
}
|
||||
|
||||
// connect to remote tcp server
|
||||
upstream = net.createConnection(port, host);
|
||||
|
||||
// reconnect internal
|
||||
connect_internal();
|
||||
|
||||
upstream.on('end', function() {
|
||||
console.log('> upstream connection terminated');
|
||||
|
||||
// sever connection to internal server
|
||||
// on reconnect we will re-establish
|
||||
internal.end();
|
||||
|
||||
setTimeout(function() {
|
||||
connect_proxy();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function connect_internal() {
|
||||
|
||||
internal = net.createConnection(local_port);
|
||||
internal.on('error', function(err) {
|
||||
console.log('error connecting to local server. retrying in 1s');
|
||||
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
ev.emit('url', body.url);
|
||||
});
|
||||
|
||||
internal.on('end', function() {
|
||||
console.log('disconnected from local server. retrying in 1s');
|
||||
setTimeout(function() {
|
||||
connect_internal();
|
||||
}, 1000);
|
||||
});
|
||||
return ev;
|
||||
};
|
||||
|
||||
upstream.pipe(internal);
|
||||
internal.pipe(upstream);
|
||||
}
|
||||
module.exports.connect = connect;
|
||||
|
||||
|
||||
2
index.js
Normal file
2
index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
module.exports.client = require('./client');
|
||||
module.exports.server = require('./server');
|
||||
12
lib/rand_id.js
Normal file
12
lib/rand_id.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
18
package.json
18
package.json
@@ -2,7 +2,7 @@
|
||||
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||
"name": "localtunnel",
|
||||
"description": "expose localhost to the world",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/shtylman/localtunnel.git"
|
||||
@@ -10,14 +10,22 @@
|
||||
"dependencies": {
|
||||
"request": "2.11.4",
|
||||
"book": "1.2.0",
|
||||
"optimist": "0.3.4"
|
||||
"optimist": "0.3.4",
|
||||
"http-raw": "1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.6.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --ui qunit -- test",
|
||||
"start": "./bin/server"
|
||||
},
|
||||
"bin": {
|
||||
"lt": "./bin/lt"
|
||||
}
|
||||
"lt": "./bin/client"
|
||||
},
|
||||
"main": "./index.js"
|
||||
}
|
||||
|
||||
495
server.js
495
server.js
@@ -3,261 +3,109 @@
|
||||
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;
|
||||
|
||||
// vendor
|
||||
var log = require('book');
|
||||
var createRawServer = require('http-raw');
|
||||
|
||||
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();
|
||||
// local
|
||||
var rand_id = require('./lib/rand_id');
|
||||
|
||||
// id -> client http server
|
||||
var clients = {};
|
||||
|
||||
// id -> list of sockets waiting for a valid response
|
||||
var wait_list = {};
|
||||
|
||||
// available parsers
|
||||
var parsers = http.parsers;
|
||||
|
||||
// data going back to a client (the last client that made a request)
|
||||
function socketOnData(d, start, end) {
|
||||
// send this request to the appropriate client
|
||||
// in -> incoming request stream
|
||||
function proxy_request(client, req, res, rs, ws) {
|
||||
|
||||
var socket = this;
|
||||
var req = this._httpMessage;
|
||||
rs = rs || req.createRawStream();
|
||||
ws = ws || res.createRawStream();
|
||||
|
||||
var current = clients[socket.subdomain].current;
|
||||
// socket is a tcp connection back to the user hosting the site
|
||||
var sock = client.sockets.shift();
|
||||
|
||||
if (!current) {
|
||||
log.error('no current for http response from backend');
|
||||
// queue request
|
||||
if (!sock) {
|
||||
log.info('no more clients, queued: %s', req.url);
|
||||
rs.pause();
|
||||
client.waiting.push([req, res, rs, ws]);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the goodies
|
||||
current.write(d.slice(start, end));
|
||||
log.info('handle req: %s', req.url);
|
||||
|
||||
// invoke parsing so we know when all the goodies have been sent
|
||||
var parser = current.out_parser;
|
||||
parser.socket = socket;
|
||||
// pipe incoming request into tcp socket
|
||||
// incoming request isn't allowed to end the socket back to lt client
|
||||
rs.pipe(sock, { end: false });
|
||||
|
||||
var ret = parser.execute(d, start, end - start);
|
||||
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) {
|
||||
debug('parse error');
|
||||
freeParser(parser, req);
|
||||
log.error(ret);
|
||||
parsers.free(parser);
|
||||
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;
|
||||
}
|
||||
}
|
||||
var handle_req = function (req, res) {
|
||||
|
||||
// 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) {
|
||||
var max_tcp_sockets = req.socket.server.max_tcp_sockets;
|
||||
|
||||
// ignore favicon
|
||||
if (req.url === '/favicon.ico') {
|
||||
@@ -265,14 +113,39 @@ server.on('request', function(req, res) {
|
||||
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
|
||||
@@ -285,17 +158,18 @@ server.on('request', function(req, res) {
|
||||
}
|
||||
|
||||
var id = requested_id || rand_id();
|
||||
if (wait_list[id]) {
|
||||
// new id
|
||||
|
||||
// if the id already exists, this client must use something else
|
||||
if (clients[id]) {
|
||||
id = rand_id();
|
||||
}
|
||||
|
||||
// generate new shit for client
|
||||
if (wait_list[id]) {
|
||||
wait_list[id].forEach(function(waiting) {
|
||||
waiting.end();
|
||||
});
|
||||
}
|
||||
// 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() {
|
||||
@@ -305,39 +179,146 @@ server.on('request', function(req, res) {
|
||||
var url = 'http://' + id + '.' + req.headers.host;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ url: url, id: id, port: port }));
|
||||
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
|
||||
var conn_timeout = setTimeout(function() {
|
||||
client_server.close();
|
||||
}, 5000);
|
||||
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) {
|
||||
|
||||
// who the info should route back to
|
||||
socket.subdomain = id;
|
||||
|
||||
// multiplexes socket data out to clients
|
||||
socket.ondata = socketOnData;
|
||||
|
||||
clearTimeout(conn_timeout);
|
||||
// no more socket connections allowed
|
||||
if (client.sockets.length >= max_tcp_sockets) {
|
||||
return socket.end();
|
||||
}
|
||||
|
||||
log.trace('new connection for id: %s', id);
|
||||
clients[id] = socket;
|
||||
wait_list[id] = [];
|
||||
|
||||
socket.on('end', function() {
|
||||
delete clients[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('err', function(err) {
|
||||
client_server.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
server.listen(argv.port, function() {
|
||||
log.info('server listening on port: %d', server.address().port);
|
||||
});
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
99
test/basic.js
Normal file
99
test/basic.js
Normal file
@@ -0,0 +1,99 @@
|
||||
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
Normal file
105
test/queue.js
Normal file
@@ -0,0 +1,105 @@
|
||||
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