remove server components

moved to localtunnel-server repo
This commit is contained in:
Roman Shtylman
2013-06-17 02:09:01 -04:00
parent 2fcac1336c
commit ed5aa3f16b
10 changed files with 18 additions and 645 deletions

View File

@@ -1,4 +1,4 @@
# localtunnel [![Build Status](https://secure.travis-ci.org/shtylman/localtunnel.png)](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.
@@ -24,43 +24,9 @@ Thats it! It will connect to the tunnel server, setup the tunnel, and tell you w
You can restart your local server all you want, ```lt``` is smart enough to detect this and reconnect once it is back.
### custom server
The default localtunnel client connects to the ```localtunnel.me``` server. You can however easily setup and run your own server. In order to run your own localtunnel server you must ensure that your server can meet the following requirements:
* You can setup DNS entries for your domain.tld and for *.domain.tld (or sub.domain.tld and *.sub.domain.tld)
* The server can accept incoming TCP connections for any non-root TCP port (ports over 1000).
The above are important as the client will ask the server for a subdomain under a particular domain. The server will listen on any OS assigned TCP port for client connections
#### setup
```shell
// pick a place where the files will live
git clone git://github.com/shtylman/localtunnel.git
cd localtunnel
npm install
// server set to run on port 1234
bin/server --port 1324
```
The localtunnel server is now running and waiting for client requests on port 1234. You will most likely want to setup a reverse proxy to listen on port 80 (or start localtunnel on port 80 directly).
#### use your server
You can now use your domain with the ```--host``` flag for the ```lt``` client.
```shell
lt --host http://sub.example.tld:1234 --port 9000
```
You will be assigned a url similar to ```qdci.sub.example.com:1234```
If your server is being a reverse proxy (i.e. nginx) and is able to listen on port 80, then you do not need the ```:1234``` part of the hostname for the ```lt``` client
## 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
var lt_client = require('localtunnel').client;
@@ -82,3 +48,10 @@ client.on('error', function(err) {
// uh oh!
});
```
## server ##
See shtylman/localtunnel-server for details on the server that powers localtunnel.
## License ##
MIT

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
var lt_client = require(__dirname + '/../client');
var lt_client = require('../client');
var argv = require('optimist')
.usage('Usage: $0 --port [num]')

View File

@@ -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

View File

@@ -1,9 +1,9 @@
// builtin
var net = require('net');
var url = require('url');
var request = require('request');
var EventEmitter = require('events').EventEmitter;
var request = require('request');
// request upstream url and connection info
var request_url = function(params, cb) {
request(params, function(err, res, body) {
@@ -140,3 +140,7 @@ var connect = function(opt) {
module.exports.connect = connect;
// for backwards compatibility
// old localtunnel modules had server and client code in same module
// so to keep .client working we expose it here
module.exports.client = module.exports;

View File

@@ -1,2 +0,0 @@
module.exports.client = require('./client');
module.exports.server = require('./server');

View File

@@ -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;
}

View File

@@ -9,24 +9,12 @@
},
"dependencies": {
"request": "2.11.4",
"book": "1.2.0",
"optimist": "0.3.4",
"http-raw": "1.1.0",
"debug": "0.7.2"
"optimist": "0.3.4"
},
"devDependencies": {
"mocha": "1.6.0"
},
"optionalDependencies": {},
"engines": {
"node": "*"
},
"scripts": {
"test": "mocha --ui qunit -- test",
"start": "./bin/server"
},
"bin": {
"lt": "./bin/client"
},
"main": "./index.js"
"main": "./client.js"
}

339
server.js
View File

@@ -1,339 +0,0 @@
var http = require('http');
var net = require('net');
var url = require('url');
var log = require('book');
var debug = require('debug')('localtunnel-server');
var createRawServer = require('http-raw');
var rand_id = require('./lib/rand_id');
// here be dragons, understanding of node http internals will be required
var HTTPParser = process.binding('http_parser').HTTPParser;
// id -> client http server
var clients = {};
// available parsers for requests
// this is borrowed from how node does things by preallocating parsers
var parsers = http.parsers;
// send this request to the appropriate client
// in -> incoming request stream
function proxy_request(client, req, res, rs, ws) {
// socket is a tcp connection back to the user hosting the site
var sock = client.sockets.shift();
// queue request
if (!sock) {
debug('no more clients, queued: %s', req.url);
rs.pause();
client.waiting.push([req, res, rs, ws]);
return;
}
debug('handle req: %s', req.url);
// pipe incoming request into tcp socket
// incoming request will close the socket when done
// lt client should establish a new socket once request is finished
// we do this instead of keeping socket open to make things easier
rs.pipe(sock);
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() {
debug('ended response: %s', req.url);
// any request we had going on is now done
ws.end();
sock.end();
// no more forwarding
delete sock.ws;
delete sock.req;
delete parser.onIncoming;
};
};
rs.resume();
}
function upstream_response(d, start, end) {
var socket = this;
var ws = socket.ws;
if (!ws) {
return log.warn('no stream set for req:', socket.req.url);
}
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;
// without a hostname, we won't know who the request is for
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
// we use 502 error to the client to signify we can't service the request
if (!client) {
debug('no client found for id: ' + client_id);
res.statusCode = 502;
return res.end('localtunnel error: no active client for \'' + client_id + '\'');
}
var rs = req.createRawStream();
var ws = res.createRawStream();
return proxy_request(client, req, res, rs, ws);
}
/// NOTE: everything below is for new client setup (not proxied request)
// ignore favicon requests
if (req.url === '/favicon.ico') {
res.writeHead(404);
return res.end();
}
var parsed = url.parse(req.url, true);
// redirect main page to github reference for root requests
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 is assigned a random id
if (clients[id]) {
id = rand_id();
}
// sockets is a list of available sockets for the connection
// waiting is a list of requests still needing to be handled
var client = clients[id] = {
sockets: [],
waiting: []
};
// new tcp server to service requests for this client
var client_server = net.createServer();
client_server.listen(function() {
var port = client_server.address().port;
debug('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({
// full url for internet facing requests
url: url,
// "subdomain" part
id: id,
// port for lt client tcp connections
port: port,
// maximum number of tcp connections allowed by lt client
max_conn_count: max_tcp_sockets
}));
});
// track initial user connection setup
var conn_timeout;
// user has 5 seconds to connect before their slot is given up
function maybe_tcp_close() {
clearTimeout(conn_timeout);
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];
// clear waiting by ending responses, (requests?)
client.waiting.forEach(function(waiting) {
waiting[1].end();
waiting[3].end(); // write stream
});
});
// new tcp connection from lt client
client_server.on('connection', function(socket) {
// no more socket connections allowed
if (client.sockets.length >= max_tcp_sockets) {
return socket.end();
}
debug('new connection for id: %s', id);
// a single connection is enough to keep client id slot open
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;
socket.once('close', function(had_error) {
debug('client %s closed socket (error: %s)', id, had_error);
// what if socket was servicing a request at this time?
// then it will be put back in available after right?
// remove this socket
var idx = client.sockets.indexOf(socket);
if (idx >= 0) {
client.sockets.splice(idx, 1);
}
// need to track total sockets, not just active available
debug('remaining client sockets: %s', client.sockets.length);
// no more sockets for this ident
if (client.sockets.length === 0) {
debug('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.sockets.push(socket);
var next = client.waiting.shift();
if (next) {
debug('handling queued request');
proxy_request(client, next[0], next[1], next[2], next[3]);
}
});
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();
});
stream.pipe(socket);
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;
};

View File

@@ -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();
});

View File

@@ -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();
});