From 93d62b9dbb9f220cfaf03f29d3142f0e32f5f09e Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Sun, 17 Jun 2012 22:46:05 -0400 Subject: [PATCH] init --- .gitignore | 1 + bin/lt | 2 + client.js | 88 ++++++++++++++++ package.json | 23 +++++ server.js | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 393 insertions(+) create mode 100644 .gitignore create mode 100755 bin/lt create mode 100644 client.js create mode 100644 package.json create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/bin/lt b/bin/lt new file mode 100755 index 0000000..521c074 --- /dev/null +++ b/bin/lt @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require(__dirname + '/../client'); diff --git a/client.js b/client.js new file mode 100644 index 0000000..60580bf --- /dev/null +++ b/client.js @@ -0,0 +1,88 @@ +// builtin +var net = require('net'); +var url = require('url'); +var request = require('request'); + +var argv = require('optimist') + .usage('Usage: $0 --port [num]') + .demand(['port']) + .options('host', { + default: 'http://lt.defunctzombie.com', + describe: 'upstream server providing forwarding' + }) + .describe('port', 'internal http server port') + .argv; + +// 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 +}; + +opt.uri = 'http://' + opt.host + ':' + opt.port + opt.path; + +var internal; +var upstream; + +(function connect_proxy() { + request(opt, function(err, res, body) { + if (err) { + console.error('upstream not available: http status %d', res.statusCode); + return process.exit(-1); + } + + // our assigned hostname and tcp port + var port = body.port; + var host = opt.host; + + console.log('your url is: %s', body.url); + + // connect to remote tcp server + upstream = net.createConnection(port, host); + + // reconnect internal + connect_internal(); + + upstream.on('end', function() { + + // 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); + }); + + internal.on('end', function() { + console.log('disconnected from local server. retrying in 1s'); + setTimeout(function() { + connect_internal(); + }, 1000); + }); + + upstream.pipe(internal); + internal.pipe(upstream); +} + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9f8957f --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "author": "Roman Shtylman ", + "name": "localtunnel", + "description": "expose localhost to the world", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "git://github.com/shtylman/localtunnel.git" + }, + "dependencies": { + "request": "2.9.202", + "book": "1.2.0", + "optimist": "0.3.4" + }, + "devDependencies": {}, + "optionalDependencies": {}, + "engines": { + "node": "*" + }, + "bin": { + "lt": "./bin/lt" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..bb4b322 --- /dev/null +++ b/server.js @@ -0,0 +1,279 @@ + +// builtin +var http = require('http'); +var net = require('net'); +var FreeList = require('freelist').FreeList; + +// 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 = new HTTPParser(HTTPParser.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; + var match = hostname.match(/^([a-z]{4})[.].*/); + + if (!match) { + // normal processing if not proxy + var res = new ServerResponse(req); + 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) { + + // generate new shit for client + var id = 'asdf'; + //rand_id(); + // + // + 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, 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(8000); +