diff --git a/README.md b/README.md index 718a765..91bb469 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/client b/bin/client index 4b23867..427bd02 100755 --- a/bin/client +++ b/bin/client @@ -1,5 +1,5 @@ #!/usr/bin/env node -var lt_client = require(__dirname + '/../client'); +var lt_client = require('../client'); var argv = require('optimist') .usage('Usage: $0 --port [num]') diff --git a/bin/server b/bin/server deleted file mode 100755 index 90c72b1..0000000 --- a/bin/server +++ /dev/null @@ -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 - diff --git a/client.js b/client.js index bb2f25b..90e98c8 100644 --- a/client.js +++ b/client.js @@ -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; diff --git a/index.js b/index.js deleted file mode 100644 index 4d51bbc..0000000 --- a/index.js +++ /dev/null @@ -1,2 +0,0 @@ -module.exports.client = require('./client'); -module.exports.server = require('./server'); diff --git a/lib/rand_id.js b/lib/rand_id.js deleted file mode 100644 index 25f4815..0000000 --- a/lib/rand_id.js +++ /dev/null @@ -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; -} - diff --git a/package.json b/package.json index eade7a9..98c2be1 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/server.js b/server.js deleted file mode 100644 index 0f01821..0000000 --- a/server.js +++ /dev/null @@ -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; -}; - diff --git a/test/basic.js b/test/basic.js deleted file mode 100644 index 34434e3..0000000 --- a/test/basic.js +++ /dev/null @@ -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(); -}); - diff --git a/test/queue.js b/test/queue.js deleted file mode 100644 index c6268b1..0000000 --- a/test/queue.js +++ /dev/null @@ -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