From f8fa049966a790c581dce9e60e2c45e0a997df95 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Wed, 4 Nov 2015 20:01:01 -0800 Subject: [PATCH] remove bouncy rework request proxy to use native http request and direct socket pipe. Cuts out bouncy which is no longer maintained and simplifies the code path. --- lib/BindingAgent.js | 23 ++++++++++ package.json | 19 ++++---- server.js | 109 ++++++++++++++++++++++++++++---------------- test/basic.js | 12 ++--- test/queue.js | 12 ++--- test/simple.js | 68 +++++++++++++++++++++++++++ test/websocket.js | 25 +++++++--- 7 files changed, 200 insertions(+), 68 deletions(-) create mode 100644 lib/BindingAgent.js create mode 100644 test/simple.js diff --git a/lib/BindingAgent.js b/lib/BindingAgent.js new file mode 100644 index 0000000..8aa9ac2 --- /dev/null +++ b/lib/BindingAgent.js @@ -0,0 +1,23 @@ +var http = require('http'); +var util = require('util'); +var assert = require('assert'); + +// binding agent will return a given options.socket as the socket for the agent +// this is useful if you already have a socket established and want the request +// to use that socket instead of making a new one +function BindingAgent(options) { + options = options || {}; + http.Agent.call(this, options); + + this.socket = options.socket; + assert(this.socket, 'socket is required for BindingAgent'); + this.createConnection = create_connection; +} + +util.inherits(BindingAgent, http.Agent); + +function create_connection(port, host, options) { + return this.socket; +} + +module.exports = BindingAgent; diff --git a/package.json b/package.json index b96f46c..c3cc4a8 100644 --- a/package.json +++ b/package.json @@ -11,25 +11,24 @@ "dependencies": { "book": "1.3.1", "book-git": "0.0.2", - "book-raven": "1.0.1", + "book-raven": "1.1.0", "bookrc": "0.0.1", - "bouncy": "3.2.2", - "debug": "2.1.0", - "express": "4.10.5", - "http-proxy": "1.11.1", + "debug": "2.2.0", + "express": "4.13.3", + "http-proxy": "1.12.0", "localenv": "0.2.2", - "on-finished": "2.2.0", + "on-finished": "2.3.0", "optimist": "0.6.1", - "stackup": "0.0.5", - "tldjs": "1.5.1" + "stackup": "1.0.1", + "tldjs": "1.6.1" }, "devDependencies": { "mocha": "2.0.1", "localtunnel": "1.8.0", - "ws": "0.6.5" + "ws": "0.8.0" }, "scripts": { - "test": "mocha --ui qunit --reporter list -- test", + "test": "mocha --ui qunit --reporter spec", "start": "./bin/server" } } diff --git a/server.js b/server.js index 17c2d9f..528584e 100644 --- a/server.js +++ b/server.js @@ -1,10 +1,12 @@ var log = require('bookrc'); var express = require('express'); -var bouncy = require('bouncy'); var tldjs = require('tldjs'); var on_finished = require('on-finished'); var debug = require('debug')('localtunnel-server'); var http_proxy = require('http-proxy'); +var http = require('http'); + +var BindingAgent = require('./lib/BindingAgent'); var proxy = http_proxy.createProxyServer({ target: 'http://localtunnel.github.io' @@ -34,7 +36,7 @@ var stats = { tunnels: 0 }; -function maybe_bounce(req, res, bounce) { +function maybe_bounce(req, res, sock, head) { // without a hostname, we won't know who the request is for var hostname = req.headers.host; if (!hostname) { @@ -58,17 +60,23 @@ function maybe_bounce(req, res, bounce) { return true; } - // flag if we already finished before we get a socket - // we can't respond to these requests var finished = false; - on_finished(res, function(err) { - if (req.headers['upgrade'] == 'websocket') { - return; - } + if (sock) { + sock.once('end', function() { + finished = true; + }); + } - finished = true; - req.connection.destroy(); - }); + if (res) { + // flag if we already finished before we get a socket + // we can't respond to these requests + on_finished(res, function(err) { + finished = true; + req.connection.destroy(); + }); + } + + // TODO add a timeout, if we run out of sockets, then just 502 // get client port client.next_socket(function(socket, done) { @@ -91,26 +99,46 @@ function maybe_bounce(req, res, bounce) { return; } - var stream = bounce(socket, { headers: { connection: 'close' } }); - - stream.on('error', function(err) { - socket.destroy(); - req.connection.destroy(); - done(); - }); - - // return the socket to the client pool - stream.once('end', function() { - done(); - }); - - on_finished(res, function(err) { - if (err) { - req.connection.destroy(); - socket.destroy(); - done(); + // websocket requests are special in that we simply re-create the header info + // and directly pipe the socket data + // avoids having to rebuild the request and handle upgrades via the http client + if (res === null) { + var arr = [req.method + ' ' + req.url + ' HTTP/' + req.httpVersion]; + for (var i=0 ; i < (req.rawHeaders.length-1) ; i+=2) { + arr.push(req.rawHeaders[i] + ': ' + req.rawHeaders[i+1]); } + + arr.push(''); + arr.push(''); + + socket.pipe(sock).pipe(socket); + socket.write(arr.join('\r\n')); + socket.once('end', function() { + done(); + }); + + return; + } + + var agent = new BindingAgent({ + socket: socket }); + + var opt = { + path: req.url, + agent: agent, + method: req.method, + headers: req.headers + }; + + var client_req = http.request(opt, function(client_res) { + client_res.pipe(res); + on_finished(client_res, function(err) { + done(); + }); + }); + + req.pipe(client_req); }); return true; @@ -187,7 +215,7 @@ module.exports = function(opt) { }); app.get('/:req_id', function(req, res, next) { - var req_id = req.param('req_id'); + var req_id = req.params.req_id; // limit requested hostnames to 20 characters if (! /^[a-z0-9]{4,20}$/.test(req_id)) { @@ -216,20 +244,23 @@ module.exports = function(opt) { }); }); - var app_port = 0; - var app_server = app.listen(app_port, function() { - app_port = app_server.address().port; - }); + var server = http.createServer(); - var server = bouncy(function(req, res, bounce) { + server.on('request', function(req, res) { debug('request %s', req.url); - - // if we should bounce this request, then don't send to our server - if (maybe_bounce(req, res, bounce)) { + if (maybe_bounce(req, res, null, null)) { return; }; - bounce(app_port); + app(req, res); + }); + + server.on('upgrade', function(req, socket, head) { + if (maybe_bounce(req, null, socket, head)) { + return; + }; + + socket.destroy(); }); return server; diff --git a/test/basic.js b/test/basic.js index 07ab292..b9e55ea 100644 --- a/test/basic.js +++ b/test/basic.js @@ -5,12 +5,13 @@ var localtunnel = require('localtunnel'); var localtunnel_server = require('../server')(); +suite('basic'); + var lt_server_port -test('set up localtunnel server', function(done) { +before('set up localtunnel server', function(done) { var server = localtunnel_server.listen(function() { lt_server_port = server.address().port; - console.log('lt server on:', lt_server_port); done(); }); }); @@ -42,7 +43,7 @@ test('landing page', function(done) { req.end(); }); -test('set up local http server', function(done) { +before('set up local http server', function(done) { var server = http.createServer(); server.on('request', function(req, res) { res.write('foo'); @@ -52,12 +53,11 @@ test('set up local http server', function(done) { var port = server.address().port; test._fake_port = port; - console.log('local http on:', port); done(); }); }); -test('set up localtunnel client', function(done) { +before('set up localtunnel client', function(done) { var opt = { host: 'http://localhost:' + lt_server_port, }; @@ -144,6 +144,6 @@ test('request uppercase domain', function(done) { }); }); -test('shutdown', function() { +after('shutdown', function() { localtunnel_server.close(); }); diff --git a/test/queue.js b/test/queue.js index 30fc677..103891c 100644 --- a/test/queue.js +++ b/test/queue.js @@ -3,6 +3,8 @@ var url = require('url'); var assert = require('assert'); var localtunnel = require('localtunnel'); +suite('queue'); + var localtunnel_server = require('../server')({ max_tcp_sockets: 1 }); @@ -10,15 +12,14 @@ var localtunnel_server = require('../server')({ var server; var lt_server_port; -test('set up localtunnel server', function(done) { +before('set up localtunnel server', function(done) { var lt_server = localtunnel_server.listen(function() { lt_server_port = lt_server.address().port; - console.log('lt server on:', lt_server_port); done(); }); }); -test('set up local http server', function(done) { +before('set up local http server', function(done) { server = http.createServer(); server.on('request', function(req, res) { // respond sometime later @@ -32,12 +33,11 @@ test('set up local http server', function(done) { var port = server.address().port; test._fake_port = port; - console.log('local http on:', port); done(); }); }); -test('set up localtunnel client', function(done) { +before('set up localtunnel client', function(done) { var opt = { host: 'http://localhost:' + lt_server_port, }; @@ -97,7 +97,7 @@ test('query localtunnel server w/ ident', function(done) { } }); -test('shutdown', function() { +after('shutdown', function() { localtunnel_server.close(); }); diff --git a/test/simple.js b/test/simple.js new file mode 100644 index 0000000..5dab5e2 --- /dev/null +++ b/test/simple.js @@ -0,0 +1,68 @@ +var http = require('http'); +var url = require('url'); +var assert = require('assert'); +var localtunnel = require('localtunnel'); + +var localtunnel_server = require('../server')({ + max_tcp_sockets: 2 +}); + +var lt_server_port + +suite('simple'); + +test('set up localtunnel server', function(done) { + var server = localtunnel_server.listen(function() { + lt_server_port = server.address().port; + done(); + }); +}); + +test('set up local http server', function(done) { + var server = http.createServer(function(req, res) { + res.end('hello world!'); + }); + + server.listen(function() { + test._fake_port = server.address().port; + done(); + }); +}); + +test('set up localtunnel client', function(done) { + var opt = { + host: 'http://localhost:' + lt_server_port, + }; + + localtunnel(test._fake_port, opt, function(err, tunnel) { + assert.ifError(err); + var url = tunnel.url; + assert.ok(new RegExp('^http:\/\/.*localhost:' + lt_server_port + '$').test(url)); + test._fake_url = url; + done(err); + }); +}); + +test('should respond to request', function(done) { + var hostname = url.parse(test._fake_url).hostname; + var opt = { + host: 'localhost', + port: lt_server_port, + headers: { + host: hostname + '.tld' + } + }; + + http.get(opt, function(res) { + var body = ''; + res.setEncoding('utf-8'); + res.on('data', function(chunk) { + body += chunk; + }); + + res.on('end', function() { + assert.equal(body, 'hello world!'); + done(); + }); + }); +}); diff --git a/test/websocket.js b/test/websocket.js index f8f3588..2d70f72 100644 --- a/test/websocket.js +++ b/test/websocket.js @@ -5,33 +5,43 @@ var localtunnel = require('localtunnel'); var WebSocket = require('ws'); var WebSocketServer = require('ws').Server; -var localtunnel_server = require('../server')(); +var localtunnel_server = require('../server')({ + max_tcp_sockets: 2 +}); var lt_server_port -test('set up localtunnel server', function(done) { +suite('websocket'); + +before('set up localtunnel server', function(done) { var server = localtunnel_server.listen(function() { lt_server_port = server.address().port; - console.log('lt server on:', lt_server_port); done(); }); }); -test('set up local websocket server', function(done) { - +before('set up local websocket server', function(done) { var wss = new WebSocketServer({ port: 0 }, function() { test._fake_port = wss._server.address().port; done(); }); + wss.on('error', function(err) { + done(err); + }); + wss.on('connection', function connection(ws) { + ws.on('error', function(err) { + done(err); + }); + ws.on('message', function incoming(message) { ws.send(message); }); }); }); -test('set up localtunnel client', function(done) { +before('set up localtunnel client', function(done) { var opt = { host: 'http://localhost:' + lt_server_port, }; @@ -45,13 +55,14 @@ test('set up localtunnel client', function(done) { }); }); -test('test websocket server request', function(done) { +test('websocket server request', function(done) { var hostname = url.parse(test._fake_url).hostname; var ws = new WebSocket('http://localhost:' + lt_server_port, { headers: { host: hostname + '.tld' } }); + ws.on('message', function(msg) { assert.equal(msg, 'something'); done();