diff --git a/.gitignore b/.gitignore index aa98793..468b525 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -test/config.json +config.json node_modules/ npm-debug.log diff --git a/README.md b/README.md index e270ff7..7a852f3 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ httpProxy.createServer(function (req, res, proxy) { // Buffer the request so that `data` and `end` events // are not lost during async operation(s). // - var buffer = proxy.buffer(req); + var buffer = httpProxy.buffer(req); // // Wait for two seconds then respond: this simulates @@ -357,6 +357,13 @@ server.on('upgrade', function(req, socket, head) { }); ``` +### Configuring your Socket limits + +By default, `node-http-proxy` will set a 100 socket limit for all `host:port` proxy targets. If you wish to change this you can two it in two ways: + +1. By passing the `maxSockets` option to `httpProxy.createServer()` +2. By calling `httpProxy.setMaxSockets(n)`, where `n` is the number of sockets you with to use. + ## Using node-http-proxy from the command line When you install this package with npm, a node-http-proxy binary will become available to you. Using this binary is easy with some simple options: diff --git a/bin/node-http-proxy b/bin/node-http-proxy index c165b1e..53139d2 100755 --- a/bin/node-http-proxy +++ b/bin/node-http-proxy @@ -4,7 +4,7 @@ var path = require('path'), fs = require('fs'), util = require('util'), argv = require('optimist').argv, - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../lib/node-http-proxy'); var help = [ "usage: node-http-proxy [options] ", diff --git a/examples/lib/store.js b/examples/helpers/store.js similarity index 95% rename from examples/lib/store.js rename to examples/helpers/store.js index 8144b13..62a627e 100644 --- a/examples/lib/store.js +++ b/examples/helpers/store.js @@ -1,5 +1,4 @@ -module.exports = Store // // just to make these example a little bit interesting, // make a little key value store with an http interface @@ -20,12 +19,10 @@ module.exports = Store // // TODO: cached map-reduce views and auto-magic sharding. // +var Store = module.exports = function Store () { + this.store = {}; +}; - - -function Store () { - this.store = {} -} Store.prototype = { get: function (key) { return this.store[key] diff --git a/examples/basic-proxy.js b/examples/http/basic-proxy.js similarity index 77% rename from examples/basic-proxy.js rename to examples/http/basic-proxy.js index 5827625..b890e69 100644 --- a/examples/basic-proxy.js +++ b/examples/http/basic-proxy.js @@ -27,16 +27,17 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); + +var welcome = [ + '# # ##### ##### ##### ##### ##### #### # # # #', + '# # # # # # # # # # # # # # # # ', + '###### # # # # ##### # # # # # # ## # ', + '# # # # ##### ##### ##### # # ## # ', + '# # # # # # # # # # # # # ', + '# # # # # # # # #### # # # ' +].join('\n'); -// ascii art from http://github.com/marak/asciimo -var welcome = '\ -# # ##### ##### ##### ##### ##### #### # # # # \n\ -# # # # # # # # # # # # # # # # \n\ -###### # # # # ##### # # # # # # ## # \n\ -# # # # ##### ##### ##### # # ## # \n\ -# # # # # # # # # # # # # \n\ -# # # # # # # # #### # # # \n'; util.puts(welcome.rainbow.bold); // diff --git a/examples/concurrent-proxy.js b/examples/http/concurrent-proxy.js similarity index 88% rename from examples/concurrent-proxy.js rename to examples/http/concurrent-proxy.js index 4bf5673..230dfc6 100644 --- a/examples/concurrent-proxy.js +++ b/examples/http/concurrent-proxy.js @@ -27,7 +27,7 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Basic Http Proxy Server @@ -42,23 +42,24 @@ httpProxy.createServer(9000, 'localhost').listen(8000); // -var connections = [] - , go +var connections = [], + go; http.createServer(function (req, res) { - - connections.push (function (){ + connections.push(function () { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); res.end(); - }) - process.stdout.write(connections.length + ', ') + }); + + process.stdout.write(connections.length + ', '); + if (connections.length > 110 || go) { - go = true - while(connections.length) - connections.shift()() + go = true; + while (connections.length) { + connections.shift()(); + } } - }).listen(9000); util.puts('http proxy server'.blue + ' started '.green.bold + 'on port '.blue + '8000'.yellow); diff --git a/examples/custom-proxy-error.js b/examples/http/custom-proxy-error.js similarity index 90% rename from examples/custom-proxy-error.js rename to examples/http/custom-proxy-error.js index 0fd90ad..dc439ea 100644 --- a/examples/custom-proxy-error.js +++ b/examples/http/custom-proxy-error.js @@ -27,17 +27,12 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Http Proxy Server with Latency // -var server = httpProxy.createServer(function (req, res, proxy) { - proxy.proxyRequest(req, res, { - port: 9000, - host: 'localhost' - }); -}) +var server = httpProxy.createServer(9000, 'localhost'); // // Tell the server to listen on port 8002 diff --git a/examples/forward-proxy.js b/examples/http/forward-proxy.js similarity index 96% rename from examples/forward-proxy.js rename to examples/http/forward-proxy.js index 667d672..ecc20fd 100644 --- a/examples/forward-proxy.js +++ b/examples/http/forward-proxy.js @@ -27,7 +27,7 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Setup proxy server with forwarding @@ -60,4 +60,4 @@ http.createServer(function (req, res) { util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with forward proxy'.magenta.underline); util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); -util.puts('http forward server '.blue + 'started '.green.bold + 'on port '.blue + '9001 '.yellow); +util.puts('http forward server '.blue + 'started '.green.bold + 'on port '.blue + '9001 '.yellow); \ No newline at end of file diff --git a/examples/latent-proxy.js b/examples/http/latent-proxy.js similarity index 95% rename from examples/latent-proxy.js rename to examples/http/latent-proxy.js index f2e51f8..5da5598 100644 --- a/examples/latent-proxy.js +++ b/examples/http/latent-proxy.js @@ -27,13 +27,13 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Http Proxy Server with Latency // httpProxy.createServer(function (req, res, proxy) { - var buffer = proxy.buffer(req); + var buffer = httpProxy.buffer(req); setTimeout(function() { proxy.proxyRequest(req, res, { port: 9000, diff --git a/examples/proxy-https-to-http.js b/examples/http/proxy-https-to-http.js similarity index 97% rename from examples/proxy-https-to-http.js rename to examples/http/proxy-https-to-http.js index 059c311..7018301 100644 --- a/examples/proxy-https-to-http.js +++ b/examples/http/proxy-https-to-http.js @@ -28,7 +28,7 @@ var https = require('https'), http = require('http'), util = require('util'), colors = require('colors'), - httpProxy = require('./../lib/node-http-proxy'), + httpProxy = require('../../lib/node-http-proxy'), helpers = require('./../test/helpers'); var opts = helpers.loadHttps(); diff --git a/examples/proxy-https-to-https.js b/examples/http/proxy-https-to-https.js similarity index 97% rename from examples/proxy-https-to-https.js rename to examples/http/proxy-https-to-https.js index d93cab8..cde2e12 100644 --- a/examples/proxy-https-to-https.js +++ b/examples/http/proxy-https-to-https.js @@ -28,7 +28,7 @@ var https = require('https'), http = require('http'), util = require('util'), colors = require('colors'), - httpProxy = require('./../lib/node-http-proxy'), + httpProxy = require('../../lib/node-http-proxy'), helpers = require('./../test/helpers'); var opts = helpers.loadHttps(); diff --git a/examples/proxy-table.js b/examples/http/proxy-table.js similarity index 97% rename from examples/proxy-table.js rename to examples/http/proxy-table.js index 5036dcd..55d97ae 100644 --- a/examples/proxy-table.js +++ b/examples/http/proxy-table.js @@ -27,7 +27,7 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Http Proxy Server with Proxy Table diff --git a/examples/standalone-proxy.js b/examples/http/standalone-proxy.js similarity index 95% rename from examples/standalone-proxy.js rename to examples/http/standalone-proxy.js index 768d0b7..e39d4b0 100644 --- a/examples/standalone-proxy.js +++ b/examples/http/standalone-proxy.js @@ -27,14 +27,14 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Http Server with proxyRequest Handler and Latency // var proxy = new httpProxy.HttpProxy(); http.createServer(function (req, res) { - var buffer = proxy.buffer(req); + var buffer = httpProxy.buffer(req); setTimeout(function() { proxy.proxyRequest(req, res, { port: 9000, diff --git a/examples/bodyDecoder-middleware.js b/examples/middleware/bodyDecoder-middleware.js similarity index 100% rename from examples/bodyDecoder-middleware.js rename to examples/middleware/bodyDecoder-middleware.js diff --git a/examples/gzip-middleware.js b/examples/middleware/gzip-middleware.js similarity index 97% rename from examples/gzip-middleware.js rename to examples/middleware/gzip-middleware.js index 856e2f6..29097ec 100644 --- a/examples/gzip-middleware.js +++ b/examples/middleware/gzip-middleware.js @@ -27,7 +27,7 @@ var util = require('util'), colors = require('colors'), http = require('http'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); // // Basic Http Proxy Server diff --git a/examples/jsonp-middleware.js b/examples/middleware/jsonp-middleware.js similarity index 98% rename from examples/jsonp-middleware.js rename to examples/middleware/jsonp-middleware.js index 11cc658..4cc31dd 100644 --- a/examples/jsonp-middleware.js +++ b/examples/middleware/jsonp-middleware.js @@ -27,4 +27,4 @@ http.createServer(new Store().handler()).listen(7531) require('http-proxy').createServer( require('connect-jsonp')(true), 'localhost', 7531 -).listen(1337) +).listen(1337) \ No newline at end of file diff --git a/examples/url-middleware.js b/examples/middleware/url-middleware.js similarity index 98% rename from examples/url-middleware.js rename to examples/middleware/url-middleware.js index 79fdf32..06fd478 100644 --- a/examples/url-middleware.js +++ b/examples/middleware/url-middleware.js @@ -37,7 +37,7 @@ httpProxy.createServer( // This is where our middlewares go, with any options desired - in this case, // the list of routes/URLs and their destinations. // - require('proxy-by-url')({ + require('proxy-by-url')({ '/hello': { port: 9000, host: 'localhost' }, '/charlie': { port: 80, host: 'charlieistheman.com' }, '/google': { port: 80, host: 'google.com' } diff --git a/examples/url-middleware2.js b/examples/middleware/url-middleware2.js similarity index 77% rename from examples/url-middleware2.js rename to examples/middleware/url-middleware2.js index 95dfef4..fcf3df2 100644 --- a/examples/url-middleware2.js +++ b/examples/middleware/url-middleware2.js @@ -19,12 +19,11 @@ httpProxy.createServer( // // Target Http Server (to listen for requests on 'localhost') // -http.createServer( - function (req, res) { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); - res.end(); - }).listen(9000); +http.createServer(function (req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); // And finally, some colored startup output. util.puts('http proxy server'.blue + ' started '.green.bold + 'on port '.blue + '8000'.yellow); diff --git a/examples/package.json b/examples/package.json index fa7f43a..ca95fd8 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,12 +1,12 @@ { - "name": "http-proxy-examples" -, "description": "packages required to run the examples" -, "version": "0.0.0" -, "dependencies": { - "connect": "1.6" - , "connect-gzip": "0.1" - , "connect-jsonp": "0.0.5" - , "connect-restreamer": "1" - , "proxy-by-url": ">= 0.0.1" + "name": "http-proxy-examples", + "description": "packages required to run the examples", + "version": "0.0.0", + "dependencies": { + "connect": "1.6", + "connect-gzip": "0.1", + "connect-jsonp": "0.0.5", + "connect-restreamer": "1", + "proxy-by-url": ">= 0.0.1" } } \ No newline at end of file diff --git a/examples/latent-websocket-proxy.js b/examples/websocket/latent-websocket-proxy.js similarity index 96% rename from examples/latent-websocket-proxy.js rename to examples/websocket/latent-websocket-proxy.js index 2b9f0f0..3598ca9 100644 --- a/examples/latent-websocket-proxy.js +++ b/examples/websocket/latent-websocket-proxy.js @@ -28,7 +28,7 @@ var sys = require('sys'), http = require('http'), colors = require('colors'), websocket = require('./../vendor/websocket'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); try { var utils = require('socket.io/lib/socket.io/utils'), @@ -80,7 +80,7 @@ var proxyServer = http.createServer(function (req, res) { // WebSocket requests as well. // proxyServer.on('upgrade', function (req, socket, head) { - var buffer = proxy.buffer(socket); + var buffer = httpProxy.buffer(socket); setTimeout(function () { proxy.proxyWebSocketRequest(req, socket, head, { diff --git a/examples/standalone-websocket-proxy.js b/examples/websocket/standalone-websocket-proxy.js similarity index 98% rename from examples/standalone-websocket-proxy.js rename to examples/websocket/standalone-websocket-proxy.js index e750c5a..fdefa6d 100644 --- a/examples/standalone-websocket-proxy.js +++ b/examples/websocket/standalone-websocket-proxy.js @@ -28,7 +28,7 @@ var sys = require('sys'), http = require('http'), colors = require('colors'), websocket = require('./../vendor/websocket'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); try { var utils = require('socket.io/lib/socket.io/utils'), diff --git a/examples/web-socket-proxy.js b/examples/websocket/websocket-proxy.js similarity index 97% rename from examples/web-socket-proxy.js rename to examples/websocket/websocket-proxy.js index d7a8efa..a1e49c6 100644 --- a/examples/web-socket-proxy.js +++ b/examples/websocket/websocket-proxy.js @@ -28,7 +28,7 @@ var sys = require('sys'), http = require('http'), colors = require('colors'), websocket = require('./../vendor/websocket'), - httpProxy = require('./../lib/node-http-proxy'); + httpProxy = require('../../lib/node-http-proxy'); try { var utils = require('socket.io/lib/socket.io/utils'), diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 027c8e7..7b3d3be 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -28,7 +28,6 @@ var util = require('util'), http = require('http'), https = require('https'), events = require('events'), - ProxyTable = require('./proxy-table').ProxyTable, maxSockets = 100; // @@ -37,62 +36,223 @@ var util = require('util'), require('pkginfo')(module, 'version'); // -// Track our own list of agents internal to `node-http-proxy` +// ### Export the relevant objects exposed by `node-http-proxy` // -var _agents = {}; +var HttpProxy = exports.HttpProxy = require('./node-http-proxy/http-proxy').HttpProxy, + ProxyTable = exports.ProxyTable = require('./node-http-proxy/proxy-table').ProxyTable, + RoutingProxy = exports.RoutingProxy = require('./node-http-proxy/routing-proxy').RoutingProxy; // -// ### function _getAgent (host, port, secure) -// #### @host {string} Host of the agent to get -// #### @port {number} Port of the agent to get -// #### @secure {boolean} Value indicating whether or not to use HTTPS -// Retreives an agent from the `http` or `https` module -// and sets the `maxSockets` property appropriately. +// ### function createServer ([port, host, options, handler]) +// #### @port {number} **Optional** Port to use on the proxy target host. +// #### @host {string} **Optional** Host of the proxy target. +// #### @options {Object} **Optional** Options for the HttpProxy instance used +// #### @handler {function} **Optional** Request handler for the server +// Returns a server that manages an instance of HttpProxy. Flexible arguments allow for: // -function _getAgent (host, port, secure) { - var Agent, id = [host, port].join(':'); - - if (!port) { - port = secure ? 443 : 80; - } - - if (!_agents[id]) { - Agent = secure ? https.Agent : http.Agent; - - _agents[id] = new Agent({ - host: host, - port: port - }); - - _agents[id].maxSockets = maxSockets; - } - - return _agents[id]; -} - +// * `httpProxy.createServer(9000, 'localhost')` +// * `httpProxy.createServer(9000, 'localhost', options) +// * `httpPRoxy.createServer(function (req, res, proxy) { ... })` // -// ### function _getProtocol (secure, outgoing) -// #### @secure {Object|boolean} Settings for `https` -// #### @outgoing {Object} Outgoing request options -// Returns the appropriate protocol based on the settings in -// `secure`. If the protocol is `https` this function will update -// the options in `outgoing` as appropriate by adding `ca`, `key`, -// and `cert` if they exist in `secure`. -// -function _getProtocol (secure, outgoing) { - var protocol = secure ? https : http; +exports.createServer = function () { + var args = Array.prototype.slice.call(arguments), + handlers = [], + options = {}, + message, + handler, + server, + proxy, + host, + port; - if (typeof secure === 'object') { - outgoing = outgoing || {}; - ['ca', 'cert', 'key'].forEach(function (prop) { - if (secure[prop]) { - outgoing[prop] = secure[prop]; + // + // Liberally parse arguments of the form: + // + // httpProxy.createServer('localhost', 9000, callback); + // httpProxy.createServer({ host: 'localhost', port: 9000 }, callback); + // **NEED MORE HERE!!!** + // + args.forEach(function (arg) { + switch (typeof arg) { + case 'string': host = arg; break; + case 'number': port = arg; break; + case 'object': options = arg || {}; break; + case 'function': handlers.push(arg); break; + }; + }); + + // + // Helper function to create intelligent error message(s) + // for the very liberal arguments parsing performed by + // `require('http-proxy').createServer()`. + // + function validArguments() { + var conditions = { + 'port and host': function () { + return port && host; + }, + 'options.target or options.router': function () { + return options && (options.router || + (options.target && options.target.host && options.target.port)); + }, + 'or proxy handlers': function () { + return handlers && handlers.length; } - }) + } + + var missing = Object.keys(conditions).filter(function (name) { + return !conditions[name](); + }); + + if (missing.length === 3) { + message = 'Cannot proxy without ' + missing.join(', '); + return false; + } + + return true; + } + + if (!validArguments()) { + // + // If `host`, `port` and `options` are all not passed (with valid + // options) then this server is improperly configured. + // + throw new Error(message); + return; } - return protocol; -} + // + // Hoist up any explicit `host` or `port` arguments + // that have been passed in to the options we will + // pass to the `httpProxy.HttpProxy` constructor. + // + options.target = options.target || {}; + options.target.port = options.target.port || port; + options.target.host = options.target.host || host; + + if (options.target && options.target.host && options.target.port) { + // + // If an explicit `host` and `port` combination has been passed + // to `.createServer()` then instantiate a hot-path optimized + // `HttpProxy` object and add the "proxy" middleware layer. + // + proxy = new HttpProxy(options); + handlers.push(function (req, res) { + proxy.proxyRequest(req, res); + }); + } + else { + // + // If no explicit `host` or `port` combination has been passed then + // we have to assume that this is a "go-anywhere" Proxy (i.e. a `RoutingProxy`). + // + proxy = new RoutingProxy(options); + + if (options.router) { + // + // If a routing table has been supplied than we assume + // the user intends us to add the "proxy" middleware layer + // for them + // + handlers.push(function (req, res) { + proxy.proxyRequest(req, res); + }); + + proxy.on('routes', function (routes) { + server.emit('routes', routes); + }); + } + } + + // + // Create the `http[s].Server` instance which will use + // an instance of `httpProxy.HttpProxy`. + // + handler = handlers.length > 1 + ? exports.stack(handlers, proxy) + : function (req, res) { handlers[0](req, res, proxy) }; + + server = options.https + ? https.createServer(options.https, handler) + : http.createServer(handler); + + server.on('close', function () { + proxy.close(); + }); + + if (handlers.length <= 1) { + // + // If an explicit callback has not been supplied then + // automagically proxy the request using the `HttpProxy` + // instance we have created. + // + server.on('upgrade', function (req, socket, head) { + proxy.proxyWebSocketRequest(req, socket, head); + }); + } + + // + // Set the proxy on the server so it is available + // to the consumer of the server + // + server.proxy = proxy; + return server; +}; + +// +// ### function buffer (obj) +// #### @obj {Object} Object to pause events from +// Buffer `data` and `end` events from the given `obj`. +// Consumers of HttpProxy performing async tasks +// __must__ utilize this utility, to re-emit data once +// the async operation has completed, otherwise these +// __events will be lost.__ +// +// var buffer = httpProxy.buffer(req); +// fs.readFile(path, function(){ +// httpProxy.proxyRequest(req, res, host, port, buffer); +// }); +// +// __Attribution:__ This approach is based heavily on +// [Connect](https://github.com/senchalabs/connect/blob/master/lib/utils.js#L157). +// However, this is not a big leap from the implementation in node-http-proxy < 0.4.0. +// This simply chooses to manage the scope of the events on a new Object literal as opposed to +// [on the HttpProxy instance](https://github.com/nodejitsu/node-http-proxy/blob/v0.3.1/lib/node-http-proxy.js#L154). +// +exports.buffer = function (obj) { + var events = [], + onData, + onEnd; + + obj.on('data', onData = function (data, encoding) { + events.push(['data', data, encoding]); + }); + + obj.on('end', onEnd = function (data, encoding) { + events.push(['end', data, encoding]); + }); + + return { + end: function () { + obj.removeListener('data', onData); + obj.removeListener('end', onEnd); + }, + destroy: function () { + this.end(); + this.resume = function () { + console.error("Cannot resume buffer after destroying it."); + }; + + onData = onEnd = events = obj = null; + }, + resume: function () { + this.end(); + for (var i = 0, len = events.length; i < len; ++i) { + obj.emit.apply(obj, events[i]); + } + } + }; +}; // // ### function getMaxSockets () @@ -116,6 +276,13 @@ exports.setMaxSockets = function (value) { // // ### function stack (middlewares, proxy) +// #### @middlewares {Array} Array of functions to stack. +// #### @proxy {HttpProxy|RoutingProxy} Proxy instance to +// Iteratively build up a single handler to the `http.Server` +// `request` event (i.e. `function (req, res)`) by wrapping +// each middleware `layer` into a `child` middleware which +// is in invoked by the parent (i.e. predecessor in the Array). +// // adapted from https://github.com/creationix/stack // exports.stack = function stack (middlewares, proxy) { @@ -134,900 +301,94 @@ exports.stack = function stack (middlewares, proxy) { res.end('Internal Server Error'); } - console.error("error in middleware layer: %s", err.stack); + console.error('Error in middleware(s): %s', err.stack); return; } - child(req, res); - } + if (child) { + child(req, res); + } + }; + // + // Set the prototype of the `next` function to the instance + // of the `proxy` so that in can be used interchangably from + // a `connect` style callback and a true `HttpProxy` object. + // + // e.g. `function (req, res, next)` vs. `function (req, res, proxy)` + // next.__proto__ = proxy; layer(req, res, next); }; }); return handle; +}; + +// +// ### function _getAgent (host, port, secure) +// #### @options {Object} Options to use when creating the agent. +// +// { +// host: 'localhost', +// port: 9000, +// https: true, +// maxSockets: 100 +// } +// +// Createsan agent from the `http` or `https` module +// and sets the `maxSockets` property appropriately. +// +exports._getAgent = function _getAgent (options) { + if (!options || !options.host) { + throw new Error('`options.host` is required to create an Agent.'); + } + + if (!options.port) { + options.port = options.https ? 443 : 80; + } + + var Agent = options.https ? https.Agent : http.Agent, + agent; + + agent = new Agent({ + host: options.host, + port: options.port + }); + + agent.maxSockets = options.maxSockets || maxSockets; + + return agent; } // -// ### function createServer ([port, host, options, handler]) -// #### @port {number} **Optional** Port to use on the proxy target host. -// #### @host {string} **Optional** Host of the proxy target. -// #### @options {Object} **Optional** Options for the HttpProxy instance used -// #### @handler {function} **Optional** Request handler for the server -// Returns a server that manages an instance of HttpProxy. Flexible arguments allow for: +// ### function _getProtocol (options) +// #### @options {Object} Options for the proxy target. +// Returns the appropriate node.js core protocol module (i.e. `http` or `https`) +// based on the `options` supplied. // -// * `httpProxy.createServer(9000, 'localhost')` -// * `httpProxy.createServer(9000, 'localhost', options) -// * `httpPRoxy.createServer(function (req, res, proxy) { ... })` -// -exports.createServer = function () { - var args = Array.prototype.slice.call(arguments), - callback, forward, - port, host, - proxy, server, - options = {}, - middleware = [], - handler, - silent; - - args.forEach(function (arg) { - switch (typeof arg) { - case 'string': host = arg; break; - case 'number': port = arg; break; - case 'function': middleware.push(handler = callback = arg); break; - case 'object': options = arg; break; - }; - }); - - proxy = new HttpProxy(options); - - if (port && host) { - // - // If we have a target host and port for the request - // then proxy to the specified location. - // - handler = function (req, res) { - proxy.proxyRequest(req, res, { - port: port, - host: host - }); - } - - if (middleware.length) { - middleware.push(handler); - } - } - else if (proxy.proxyTable) { - // - // If the proxy is configured with a ProxyTable - // instance then use that before failing. - // - handler = function (req, res) { - proxy.proxyRequest(req, res); - } - - if (middleware.length) { - middleware.push(handler); - } - } - - if (middleware.length > 1) { - handler = callback = exports.stack(middleware, proxy); - } - else if (middleware.length) { - // - // Do not use middleware code if it's not needed. - // - var h = middleware[0]; - handler = callback = function (req,res) { h(req,res,proxy) }; - } - - if (!handler) { - // - // Otherwise this server is improperly configured. - // - throw new Error('Cannot proxy without port, host, or router.') - } - - server = options.https - ? https.createServer(options.https, handler) - : http.createServer(handler); - - server.on('close', function () { - proxy.close(); - }); - - proxy.on('routes', function (routes) { - server.emit('routes', routes); - }); - - if (!callback) { - // WebSocket support: if callback is empty tunnel - // websocket request automatically - server.on('upgrade', function (req, socket, head) { - // Tunnel websocket requests too - proxy.proxyWebSocketRequest(req, socket, head, { - port: port, - host: host - }); - }); - } - - // - // Set the proxy on the server so it is available - // to the consumer of the server - // - server.proxy = proxy; - return server; +exports._getProtocol = function _getProtocol (options) { + return options.https ? https : http; }; -// -// ### function HttpProxy (options) -// #### @options {Object} Options for this instance. -// Constructor function for new instances of HttpProxy responsible -// for managing the life-cycle of streaming reverse proxyied HTTP requests. -// -// Example options: -// -// { -// router: { -// 'foo.com': 'localhost:8080', -// 'bar.com': 'localhost:8081' -// }, -// forward: { -// host: 'localhost', -// port: 9001 -// } -// } -// -var HttpProxy = exports.HttpProxy = function (options) { - events.EventEmitter.call(this); - - var self = this; - options = options || {}; - - // - // Setup basic proxying options - // - this.https = options.https; - this.forward = options.forward; - this.target = options.target || {}; - - // - // Setup additional options for WebSocket proxying. When forcing - // the WebSocket handshake to change the `sec-websocket-location` - // and `sec-websocket-origin` headers `options.source` **MUST** - // be provided or the operation will fail with an `origin mismatch` - // by definition. - // - this.source = options.source || { host: 'localhost', port: 8000 }; - this.changeOrigin = options.changeOrigin || false; - - if (options.router) { - this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly); - this.proxyTable.on('routes', function (routes) { - self.emit('routes', routes); - }); - } -}; - -// Inherit from events.EventEmitter -util.inherits(HttpProxy, events.EventEmitter); // -// ### function buffer (obj) -// #### @obj {Object} Object to pause events from -// Buffer `data` and `end` events from the given `obj`. -// Consumers of HttpProxy performing async tasks -// __must__ utilize this utility, to re-emit data once -// the async operation has completed, otherwise these -// __events will be lost.__ +// ### function _getBase (options) +// #### @options {Object} Options for the proxy target. +// Returns the relevate base object to create on outgoing proxy request. +// If `options.https` are supplied, this function respond with an object +// containing the relevant `ca`, `key`, and `cert` properties. // -// var buffer = httpProxy.buffer(req); -// fs.readFile(path, function(){ -// httpProxy.proxyRequest(req, res, host, port, buffer); -// }); -// -// __Attribution:__ This approach is based heavily on -// [Connect](https://github.com/senchalabs/connect/blob/master/lib/utils.js#L157). -// However, this is not a big leap from the implementation in node-http-proxy < 0.4.0. -// This simply chooses to manage the scope of the events on a new Object literal as opposed to -// [on the HttpProxy instance](https://github.com/nodejitsu/node-http-proxy/blob/v0.3.1/lib/node-http-proxy.js#L154). -// -HttpProxy.prototype.buffer = function (obj) { - var onData, onEnd, events = []; - - obj.on('data', onData = function (data, encoding) { - events.push(['data', data, encoding]); - }); - - obj.on('end', onEnd = function (data, encoding) { - events.push(['end', data, encoding]); - }); - - return { - end: function () { - obj.removeListener('data', onData); - obj.removeListener('end', onEnd); - }, - destroy: function () { - this.end(); - this.resume = function () { - console.error("Cannot resume buffer after destroying it."); - }; - - onData = onEnd = events = obj = null; - }, - resume: function () { - this.end(); - for (var i = 0, len = events.length; i < len; ++i) { - obj.emit.apply(obj, events[i]); - } - } - }; -}; - -// -// ### function close () -// Frees the resources associated with this instance, -// if they exist. -// -HttpProxy.prototype.close = function () { - if (this.proxyTable) { - this.proxyTable.close(); - } -}; - -// -// ### function proxyRequest (req, res, [port, host, paused]) -// #### @req {ServerRequest} Incoming HTTP Request to proxy. -// #### @res {ServerResponse} Outgoing HTTP Request to write proxied data to. -// #### @options {Object} Options for the outgoing proxy request. -// -// options.port {number} Port to use on the proxy target host. -// options.host {string} Host of the proxy target. -// options.buffer {Object} Result from `httpProxy.buffer(req)` -// options.https {Object|boolean} Settings for https. -// options.enableXForwarded {boolean} Don't clobber x-forwarded headers to allow layered proxies. -// -HttpProxy.prototype.proxyRequest = function (req, res, options) { - var self = this, errState = false, location, outgoing, protocol, reverseProxy; - - // - // Create an empty options hash if none is passed. - // If default options have been passed to the constructor - // of this instance, use them by default. - // - options = options || {}; - options.host = options.host || this.target.host; - options.port = options.port || this.target.port; - options.enableXForwarded = - (undefined === options.enableXForwarded ? true : options.enableXForwarded); - - // - // Check the proxy table for this instance to see if we need - // to get the proxy location for the request supplied. We will - // always ignore the proxyTable if an explicit `port` and `host` - // arguments are supplied to `proxyRequest`. - // - if (this.proxyTable && !options.host) { - location = this.proxyTable.getProxyLocation(req); - - // - // If no location is returned from the ProxyTable instance - // then respond with `404` since we do not have a valid proxy target. - // - if (!location) { - try { - res.writeHead(404); - res.end(); - } - catch (er) { - console.error("res.writeHead/res.end error: %s", er.message); - } - - return; - } - - // - // When using the ProxyTable in conjunction with an HttpProxy instance - // only the following arguments are valid: - // - // * `proxy.proxyRequest(req, res, { host: 'localhost' })`: This will be skipped - // * `proxy.proxyRequest(req, res, { buffer: buffer })`: Buffer will get updated appropriately - // * `proxy.proxyRequest(req, res)`: Options will be assigned appropriately. - // - options.port = location.port; - options.host = location.host; - } - - // - // Add common proxy headers to the request so that they can - // be availible to the proxy target server: - // - // * `x-forwarded-for`: IP Address of the original request - // * `x-forwarded-proto`: Protocol of the original request - // * `x-forwarded-port`: Port of the original request. - // - if (options.enableXForwarded === true && req.connection && req.connection.socket) { - req.headers['x-forwarded-for'] = req.connection.remoteAddress || req.connection.socket.remoteAddress; - req.headers['x-forwarded-port'] = req.connection.remotePort || req.connection.socket.remotePort; - req.headers['x-forwarded-proto'] = req.connection.pair ? 'https' : 'http'; - } - - // - // Emit the `start` event indicating that we have begun the proxy operation. - // - this.emit('start', req, res, options); - - // - // If forwarding is enabled for this instance, foward proxy the - // specified request to the address provided in `this.forward` - // - if (this.forward) { - this.emit('forward', req, res, this.forward); - this._forwardRequest(req); - } - - // - // #### function proxyError (err) - // #### @err {Error} Error contacting the proxy target - // Short-circuits `res` in the event of any error when - // contacting the proxy target at `host` / `port`. - // - function proxyError(err) { - errState = true; - - // - // Emit an `error` event, allowing the application to use custom - // error handling. The error handler should end the response. - // - if (self.emit('proxyError', err, req, res)) { - return; - } - - res.writeHead(500, { 'Content-Type': 'text/plain' }); - - if (req.method !== 'HEAD') { - // - // This NODE_ENV=production behavior is mimics Express and - // Connect. - // - if (process.env.NODE_ENV === 'production') { - res.write('Internal Server Error'); - } - else { - res.write('An error has occurred: ' + JSON.stringify(err)); - } - } - - try { res.end() } - catch (er) { console.error("res.end error: %s", er.message) } - } - - outgoing = { - host: options.host, - port: options.port, - agent: _getAgent(options.host, options.port, options.https || this.target.https), - method: req.method, - path: req.url, - headers: req.headers - }; - - protocol = _getProtocol(options.https || this.target.https, outgoing); - - // Open new HTTP request to internal resource with will act as a reverse proxy pass - reverseProxy = protocol.request(outgoing, function (response) { - - // Process the `reverseProxy` `response` when it's received. - if (response.headers.connection) { - if (req.headers.connection) response.headers.connection = req.headers.connection; - else response.headers.connection = 'close'; - } - - // Set the headers of the client response - res.writeHead(response.statusCode, response.headers); - - // `response.statusCode === 304`: No 'data' event and no 'end' - if (response.statusCode === 304) { - try { res.end() } - catch (er) { console.error("res.end error: %s", er.message) } - return; - } - - // For each data `chunk` received from the `reverseProxy` - // `response` write it to the outgoing `res`. - // If the res socket has been killed already, then write() - // will throw. Nevertheless, try our best to end it nicely. - response.on('data', function (chunk) { - if (req.method !== 'HEAD' && res.writable) { - try { - var flushed = res.write(chunk); - } - catch (er) { - console.error("res.write error: %s", er.message); - - try { res.end() } - catch (er) { console.error("res.end error: %s", er.message) } - - return; - } - } - if (!flushed) { - response.pause(); - - res.once('drain', function () { - try { response.resume() } - catch (er) { console.error("response.resume error: %s", er.message) } - }); - - setTimeout(function () { - res.emit('drain'); - }, 100); - } - }); - - // When the `reverseProxy` `response` ends, end the - // corresponding outgoing `res` unless we have entered - // an error state. In which case, assume `res.end()` has - // already been called and the 'error' event listener - // removed. - response.on('end', function () { - if (!errState) { - reverseProxy.removeListener('error', proxyError); - - try { res.end() } - catch (er) { console.error("res.end error: %s", er.message) } - - // Emit the `end` event now that we have completed proxying - self.emit('end', req, res); - } - }); - }); - - // Handle 'error' events from the `reverseProxy`. - reverseProxy.once('error', proxyError); - - // For each data `chunk` received from the incoming - // `req` write it to the `reverseProxy` request. - req.on('data', function (chunk) { - if (!errState) { - var flushed = reverseProxy.write(chunk); - if (!flushed) { - req.pause(); - - reverseProxy.once('drain', function () { - try { req.resume() } - catch (er) { console.error("req.resume error: %s", er.message) } - }); - - setTimeout(function () { - reverseProxy.emit('drain'); - }, 100); - } - } - }); - - // - // When the incoming `req` ends, end the corresponding `reverseProxy` - // request unless we have entered an error state. - // - req.on('end', function () { - if (!errState) { - reverseProxy.end(); - } - }); - - // If we have been passed buffered data, resume it. - if (options.buffer) { - if (!errState) { - options.buffer.resume(); - } - else { - options.buffer.destroy(); - } - } -}; - -// -// ### @private function _forwardRequest (req) -// #### @req {ServerRequest} Incoming HTTP Request to proxy. -// Forwards the specified `req` to the location specified -// by `this.forward` ignoring errors and the subsequent response. -// -HttpProxy.prototype._forwardRequest = function (req) { - var self = this, port, host, outgoing, protocol, forwardProxy; - - port = this.forward.port; - host = this.forward.host; - - outgoing = { - host: host, - port: port, - agent: _getAgent(host, port, this.forward.https), - method: req.method, - path: req.url, - headers: req.headers - }; - - // Force the `connection` header to be 'close' until - // node.js core re-implements 'keep-alive'. - outgoing.headers['connection'] = 'close'; - - protocol = _getProtocol(this.forward.https, outgoing); - - // Open new HTTP request to internal resource with will act as a reverse proxy pass - forwardProxy = protocol.request(outgoing, function (response) { - // - // Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. - // Remark (indexzero): We will eventually emit a 'forward' event here for performance tuning. - // - }); - - // Add a listener for the connection timeout event. - // - // Remark: Ignoring this error in the event - // forward target doesn't exist. - // - forwardProxy.once('error', function (err) { }); - - // Chunk the client request body as chunks from the proxied request come in - req.on('data', function (chunk) { - var flushed = forwardProxy.write(chunk); - if (!flushed) { - req.pause(); - - forwardProxy.once('drain', function () { - try { req.resume() } - catch (er) { console.error("req.resume error: %s", er.message) } - }); - - setTimeout(function () { - forwardProxy.emit('drain'); - }, 100); - } - }) - - // At the end of the client request, we are going to stop the proxied request - req.on('end', function () { - forwardProxy.end(); - }); -}; - -// -// ### function proxyWebSocketRequest (req, socket, head, options) -// #### @req {ServerRequest} Websocket request to proxy. -// #### @socket {net.Socket} Socket for the underlying HTTP request -// #### @head {string} Headers for the Websocket request. -// #### @options {Object} Options to use when proxying this request. -// -// options.port {number} Port to use on the proxy target host. -// options.host {string} Host of the proxy target. -// options.buffer {Object} Result from `httpProxy.buffer(req)` -// options.https {Object|boolean} Settings for https. -// -HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options) { - var self = this, - listeners = {}, - errState = false, - CRLF = '\r\n', - outgoing; - - options = options || {}; - options.host = options.host || this.target.host; - options.port = options.port || this.target.port; - - if (this.proxyTable && !options.host) { - location = this.proxyTable.getProxyLocation(req); - - if (!location) { - return socket.destroy(); - } - - options.port = location.port; - options.host = location.host; - } - - // - // WebSocket requests must have the `GET` method and - // the `upgrade:websocket` header - // - if (req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket') { - // - // This request is not WebSocket request - // - return; - } - - // - // Helper function for setting appropriate socket values: - // 1. Turn of all bufferings - // 2. For server set KeepAlive - // 3. For client set encoding - // - function _socket(socket, keepAlive) { - socket.setTimeout(0); - socket.setNoDelay(true); - if (keepAlive) { - if (socket.setKeepAlive) { - socket.setKeepAlive(true, 0); - } - else if (socket.pair.cleartext.socket.setKeepAlive) { - socket.pair.cleartext.socket.setKeepAlive(true, 0); - } - } - else { - socket.setEncoding('utf8'); - } - } - - // - // On `upgrade` from the Agent socket, listen to - // the appropriate events. - // - function onUpgrade (reverseProxy, proxySocket) { - if (!reverseProxy) { - proxySocket.end(); - socket.end(); - return; - } - - // - // Any incoming data on this WebSocket to the proxy target - // will be written to the `reverseProxy` socket. - // - proxySocket.on('data', listeners.onIncoming = function (data) { - if (reverseProxy.incoming.socket.writable) { - try { - self.emit('websocket:outgoing', req, socket, head, data); - var flushed = reverseProxy.incoming.socket.write(data); - if (!flushed) { - proxySocket.pause(); - - reverseProxy.incoming.socket.once('drain', function () { - try { proxySocket.resume() } - catch (er) { console.error("proxySocket.resume error: %s", er.message) } - }); - - setTimeout(function () { - reverseProxy.incoming.socket.emit('drain'); - }, 100); - } - } - catch (ex) { - detach(); - reverseProxy.incoming.socket.end(); - proxySocket.end(); - } - } - }); - - // - // Any outgoing data on this Websocket from the proxy target - // will be written to the `proxySocket` socket. - // - reverseProxy.incoming.socket.on('data', listeners.onOutgoing = function(data) { - try { - self.emit('websocket:incoming', reverseProxy, reverseProxy.incoming, head, data); - var flushed = proxySocket.write(data); - if (!flushed) { - reverseProxy.incoming.socket.pause(); - - proxySocket.once('drain', function () { - try { reverseProxy.incoming.socket.resume() } - catch (ex) { console.error("reverseProxy.incoming.socket.resume error: %s", er.message) } - }); - - setTimeout(function () { - proxySocket.emit('drain'); - }, 100); - } - } - catch (ex) { - detach(); - proxySocket.end(); - socket.end(); - } - }); - - // - // Helper function to detach all event listeners - // from `reverseProxy` and `proxySocket`. - // - function detach() { - proxySocket.removeListener('end', listeners.onIncomingClose); - proxySocket.removeListener('data', listeners.onIncoming); - reverseProxy.incoming.socket.removeListener('end', listeners.onOutgoingClose); - reverseProxy.incoming.socket.removeListener('data', listeners.onOutgoing); - } - - // - // If the incoming `proxySocket` socket closes, then - // detach all event listeners. - // - proxySocket.on('end', listeners.onIncomingClose = function() { - reverseProxy.incoming.socket.end(); - detach(); - - // Emit the `end` event now that we have completed proxying - self.emit('websocket:end', req, socket, head); - }); - - // - // If the `reverseProxy` socket closes, then detach all - // event listeners. - // - reverseProxy.incoming.socket.on('end', listeners.onOutgoingClose = function() { - proxySocket.end(); - detach(); - }); - }; - - // Setup the incoming client socket. - _socket(socket); - - function getPort (port) { - port = port || 80; - return port - 80 === 0 ? '' : ':' + port - } - - // - // Get the protocol, and host for this request and create an instance - // of `http.Agent` or `https.Agent` from the pool managed by `node-http-proxy`. - // - var protocolName = options.https || this.target.https ? 'https' : 'http', - portUri = getPort(this.source.port), - remoteHost = options.host + portUri, - agent = _getAgent(options.host, options.port, options.https || this.target.https); - - // Change headers (if requested). - if (this.changeOrigin) { - req.headers.host = remoteHost; - req.headers.origin = protocolName + '://' + remoteHost; - } - - // - // Make the outgoing WebSocket request - // - outgoing = { - host: options.host, - port: options.port, - method: 'GET', - path: req.url, - headers: req.headers, - }; +exports._getBase = function _getBase (options) { + var result = function () {}; - var reverseProxy = agent.appendMessage(outgoing); - - // - // On any errors from the `reverseProxy` emit the - // `webSocketProxyError` and close the appropriate - // connections. - // - function proxyError (err) { - reverseProxy.end(); - if (self.emit('webSocketProxyError', req, socket, head)) { - return; - } - socket.end(); - } - - // - // Here we set the incoming `req`, `socket` and `head` data to the outgoing - // request so that we can reuse this data later on in the closure scope - // available to the `upgrade` event. This bookkeeping is not tracked anywhere - // in nodejs core and is **very** specific to proxying WebSockets. - // - reverseProxy.agent = agent; - reverseProxy.incoming = { - request: req, - socket: socket, - head: head - }; - - // - // If the agent for this particular `host` and `port` combination - // is not already listening for the `upgrade` event, then do so once. - // This will force us not to disconnect. - // - // In addition, it's important to note the closure scope here. Since - // there is no mapping of the - // - if (!agent._events || agent._events['upgrade'].length === 0) { - agent.on('upgrade', function (_, remoteSocket, head) { - // - // Prepare the socket for the reverseProxy request and begin to - // stream data between the two sockets. Here it is important to - // note that `remoteSocket._httpMessage === reverseProxy`. - // - _socket(remoteSocket, true); - onUpgrade(remoteSocket._httpMessage, remoteSocket); + if (options.https && typeof options.https === 'object') { + ['ca', 'cert', 'key'].forEach(function (key) { + if (options.https[key]) { + result.prototype[key] = options.https[key]; + } }); } - // - // If the reverseProxy connection has an underlying socket, - // then execute the WebSocket handshake. - // - if (typeof reverseProxy.socket !== 'undefined') { - reverseProxy.socket.on('data', function handshake (data) { - // - // Ok, kind of harmfull part of code. Socket.IO sends a hash - // at the end of handshake if protocol === 76, but we need - // to replace 'host' and 'origin' in response so we split - // data to printable data and to non-printable. (Non-printable - // will come after double-CRLF). - // - var sdata = data.toString(); - - // Get the Printable data - sdata = sdata.substr(0, sdata.search(CRLF + CRLF)); - - // Get the Non-Printable data - data = data.slice(Buffer.byteLength(sdata), data.length); - - if (self.https && !self.target.https) { - // - // If the proxy server is running HTTPS but the client is running - // HTTP then replace `ws` with `wss` in the data sent back to the client. - // - sdata = sdata.replace('ws:', 'wss:'); - } - - try { - // - // Write the printable and non-printable data to the socket - // from the original incoming request. - // - self.emit('websocket:handshake', req, socket, head, sdata, data); - socket.write(sdata); - var flushed = socket.write(data); - if (!flushed) { - reverseProxy.socket.pause(); - - socket.once('drain', function () { - try { reverseProxy.socket.resume() } - catch (er) { console.error("reverseProxy.socket.resume error: %s", er.message) } - }); - - setTimeout(function () { - socket.emit('drain'); - }, 100); - } - - } - catch (ex) { - proxyError(ex); - } - - // Catch socket errors - socket.on('error', proxyError); - - // Remove data listener now that the 'handshake' is complete - reverseProxy.socket.removeListener('data', handshake); - }); - } - - reverseProxy.on('error', proxyError); - - try { - // Attempt to write the upgrade-head to the reverseProxy request. - // This is small, and there's only ever one of it. - // No need for pause/resume. - reverseProxy.write(head); - } - catch (ex) { - proxyError(ex); - } - - // If we have been passed buffered data, resume it. - if (options.buffer) { - if (!errState) { - options.buffer.resume(); - } - else { - options.buffer.destroy(); - } - } -}; + return result; +}; \ No newline at end of file diff --git a/lib/node-http-proxy/http-proxy.js b/lib/node-http-proxy/http-proxy.js new file mode 100644 index 0000000..9b4ef59 --- /dev/null +++ b/lib/node-http-proxy/http-proxy.js @@ -0,0 +1,766 @@ +/* + node-http-proxy.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +var events = require('events'), + util = require('util'), + httpProxy = require('../node-http-proxy'); + +// +// ### function HttpProxy (options) +// #### @options {Object} Options for this instance. +// Constructor function for new instances of HttpProxy responsible +// for managing the life-cycle of streaming reverse proxyied HTTP requests. +// +// Example options: +// +// { +// target: { +// host: 'localhost', +// port: 9000 +// }, +// forward: { +// host: 'localhost', +// port: 9001 +// } +// } +// +var HttpProxy = exports.HttpProxy = function (options) { + if (!options || !options.target) { + throw new Error('Both `options` and `options.target` are required.'); + } + + events.EventEmitter.call(this); + + var self = this; + + // + // Setup basic proxying options: + // + // * forward {Object} Options for a forward-proxy (if-any) + // * target {Object} Options for the **sole** proxy target of this instance + // + this.forward = options.forward; + this.target = options.target; + + // + // Setup the necessary instances instance variables for + // the `target` and `forward` `host:port` combinations + // used by this instance. + // + // * agent {http[s].Agent} Agent to be used by this instance. + // * protocol {http|https} Core node.js module to make requests with. + // * base {Object} Base object to create when proxying containing any https settings. + // + function setupProxy (key) { + self[key].agent = httpProxy._getAgent(self[key]); + self[key].protocol = httpProxy._getProtocol(self[key]); + self[key].base = httpProxy._getBase(self[key]); + } + + setupProxy('target'); + if (this.forward) { + setupProxy('forward'); + } + + // + // Setup opt-in features + // + this.enable = options.enable || {}; + this.enable.xforward = typeof this.enable.xforward === 'boolean' + ? this.enable.xforward + : true; + + // + // Setup additional options for WebSocket proxying. When forcing + // the WebSocket handshake to change the `sec-websocket-location` + // and `sec-websocket-origin` headers `options.source` **MUST** + // be provided or the operation will fail with an `origin mismatch` + // by definition. + // + this.source = options.source || { host: 'localhost', port: 8000 }; + this.source.https = this.source.https || options.https; + this.changeOrigin = options.changeOrigin || false; +}; + +// Inherit from events.EventEmitter +util.inherits(HttpProxy, events.EventEmitter); + +// +// ### function proxyRequest (req, res, [port, host, paused]) +// #### @req {ServerRequest} Incoming HTTP Request to proxy. +// #### @res {ServerResponse} Outgoing HTTP Request to write proxied data to. +// #### @buffer {Object} Result from `httpProxy.buffer(req)` +// +HttpProxy.prototype.proxyRequest = function (req, res, buffer) { + var self = this, + errState = false, + outgoing = new(this.target.base), + reverseProxy; + + // + // Add common proxy headers to the request so that they can + // be availible to the proxy target server: + // + // * `x-forwarded-for`: IP Address of the original request + // * `x-forwarded-proto`: Protocol of the original request + // * `x-forwarded-port`: Port of the original request. + // + if (this.enable.xforward && req.connection && req.connection.socket) { + req.headers['x-forwarded-for'] = req.connection.remoteAddress || req.connection.socket.remoteAddress; + req.headers['x-forwarded-port'] = req.connection.remotePort || req.connection.socket.remotePort; + req.headers['x-forwarded-proto'] = req.connection.pair ? 'https' : 'http'; + } + + // + // Emit the `start` event indicating that we have begun the proxy operation. + // + this.emit('start', req, res, this.target); + + // + // If forwarding is enabled for this instance, foward proxy the + // specified request to the address provided in `this.forward` + // + if (this.forward) { + this.emit('forward', req, res, this.forward); + this._forwardRequest(req); + } + + // + // #### function proxyError (err) + // #### @err {Error} Error contacting the proxy target + // Short-circuits `res` in the event of any error when + // contacting the proxy target at `host` / `port`. + // + function proxyError(err) { + errState = true; + + // + // Emit an `error` event, allowing the application to use custom + // error handling. The error handler should end the response. + // + if (self.emit('proxyError', err, req, res)) { + return; + } + + res.writeHead(500, { 'Content-Type': 'text/plain' }); + + if (req.method !== 'HEAD') { + // + // This NODE_ENV=production behavior is mimics Express and + // Connect. + // + if (process.env.NODE_ENV === 'production') { + res.write('Internal Server Error'); + } + else { + res.write('An error has occurred: ' + JSON.stringify(err)); + } + } + + try { res.end() } + catch (ex) { console.error("res.end error: %s", ex.message) } + } + + // + // Setup outgoing proxy with relevant properties. + // + outgoing.host = this.target.host; + outgoing.port = this.target.port; + outgoing.agent = this.target.agent; + outgoing.method = req.method; + outgoing.path = req.url; + outgoing.headers = req.headers; + + // + // Open new HTTP request to internal resource with will act + // as a reverse proxy pass + // + reverseProxy = this.target.protocol.request(outgoing, function (response) { + // + // Process the `reverseProxy` `response` when it's received. + // + if (response.headers.connection) { + if (req.headers.connection) { response.headers.connection = req.headers.connection } + else { response.headers.connection = 'close' } + } + + // Set the headers of the client response + res.writeHead(response.statusCode, response.headers); + + // If `response.statusCode === 304`: No 'data' event and no 'end' + if (response.statusCode === 304) { + try { res.end() } + catch (ex) { console.error("res.end error: %s", ex.message) } + return; + } + + // + // For each data `chunk` received from the `reverseProxy` + // `response` write it to the outgoing `res`. + // If the res socket has been killed already, then write() + // will throw. Nevertheless, try our best to end it nicely. + // + response.on('data', function (chunk) { + if (req.method !== 'HEAD' && res.writable) { + try { + var flushed = res.write(chunk); + } + catch (ex) { + console.error("res.write error: %s", ex.message); + + try { res.end() } + catch (ex) { console.error("res.end error: %s", ex.message) } + + return; + } + + if (!flushed) { + response.pause(); + res.once('drain', function () { + try { response.resume() } + catch (er) { console.error("response.resume error: %s", er.message) } + }); + + // + // Force the `drain` event in 100ms if it hasn't + // happened on its own. + // + setTimeout(function () { + res.emit('drain'); + }, 100); + } + } + }); + + // + // When the `reverseProxy` `response` ends, end the + // corresponding outgoing `res` unless we have entered + // an error state. In which case, assume `res.end()` has + // already been called and the 'error' event listener + // removed. + // + response.on('end', function () { + if (!errState) { + reverseProxy.removeListener('error', proxyError); + + try { res.end() } + catch (ex) { console.error("res.end error: %s", ex.message) } + + // Emit the `end` event now that we have completed proxying + self.emit('end', req, res); + } + }); + }); + + // + // Handle 'error' events from the `reverseProxy`. + // + reverseProxy.once('error', proxyError); + + // + // For each data `chunk` received from the incoming + // `req` write it to the `reverseProxy` request. + // + req.on('data', function (chunk) { + if (!errState) { + var flushed = reverseProxy.write(chunk); + if (!flushed) { + req.pause(); + reverseProxy.once('drain', function () { + try { req.resume() } + catch (er) { console.error("req.resume error: %s", er.message) } + }); + + // + // Force the `drain` event in 100ms if it hasn't + // happened on its own. + // + setTimeout(function () { + reverseProxy.emit('drain'); + }, 100); + } + } + }); + + // + // When the incoming `req` ends, end the corresponding `reverseProxy` + // request unless we have entered an error state. + // + req.on('end', function () { + if (!errState) { + reverseProxy.end(); + } + }); + + // + // If we have been passed buffered data, resume it. + // + if (buffer) { + return !errState + ? buffer.resume() + : buffer.destroy(); + } +}; + +// +// ### function proxyWebSocketRequest (req, socket, head, buffer) +// #### @req {ServerRequest} Websocket request to proxy. +// #### @socket {net.Socket} Socket for the underlying HTTP request +// #### @head {string} Headers for the Websocket request. +// #### @buffer {Object} Result from `httpProxy.buffer(req)` +// Performs a WebSocket proxy operation to the location specified by +// `this.target`. +// +HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, buffer) { + var self = this, + outgoing = new(this.target.base), + listeners = {}, + errState = false, + CRLF = '\r\n'; + + // + // WebSocket requests must have the `GET` method and + // the `upgrade:websocket` header + // + if (req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket') { + // + // This request is not WebSocket request + // + return socket.destroy(); + } + + // + // Add common proxy headers to the request so that they can + // be availible to the proxy target server: + // + // * `x-forwarded-for`: IP Address of the original request + // * `x-forwarded-proto`: Protocol of the original request + // * `x-forwarded-port`: Port of the original request. + // + if (this.enable.xforward && req.connection && req.connection.socket) { + req.headers['x-forwarded-for'] = req.connection.remoteAddress || req.connection.socket.remoteAddress; + req.headers['x-forwarded-port'] = req.connection.remotePort || req.connection.socket.remotePort; + req.headers['x-forwarded-proto'] = req.connection.pair ? 'https' : 'http'; + } + + // + // Helper function for setting appropriate socket values: + // 1. Turn of all bufferings + // 2. For server set KeepAlive + // 3. For client set encoding + // + function _socket(socket, keepAlive) { + socket.setTimeout(0); + socket.setNoDelay(true); + + if (keepAlive) { + if (socket.setKeepAlive) { + socket.setKeepAlive(true, 0); + } + else if (socket.pair.cleartext.socket.setKeepAlive) { + socket.pair.cleartext.socket.setKeepAlive(true, 0); + } + } + else { + socket.setEncoding('utf8'); + } + } + + // + // Setup the incoming client socket. + // + _socket(socket); + + // + // On `upgrade` from the Agent socket, listen to + // the appropriate events. + // + function onUpgrade (reverseProxy, proxySocket) { + if (!reverseProxy) { + proxySocket.end(); + socket.end(); + return; + } + + // + // Any incoming data on this WebSocket to the proxy target + // will be written to the `reverseProxy` socket. + // + proxySocket.on('data', listeners.onIncoming = function (data) { + if (reverseProxy.incoming.socket.writable) { + try { + self.emit('websocket:outgoing', req, socket, head, data); + var flushed = reverseProxy.incoming.socket.write(data); + if (!flushed) { + proxySocket.pause(); + reverseProxy.incoming.socket.once('drain', function () { + try { proxySocket.resume() } + catch (er) { console.error("proxySocket.resume error: %s", er.message) } + }); + + // + // Force the `drain` event in 100ms if it hasn't + // happened on its own. + // + setTimeout(function () { + reverseProxy.incoming.socket.emit('drain'); + }, 100); + } + } + catch (ex) { + detach(); + reverseProxy.incoming.socket.end(); + proxySocket.end(); + } + } + }); + + // + // Any outgoing data on this Websocket from the proxy target + // will be written to the `proxySocket` socket. + // + reverseProxy.incoming.socket.on('data', listeners.onOutgoing = function (data) { + try { + self.emit('websocket:incoming', reverseProxy, reverseProxy.incoming, head, data); + var flushed = proxySocket.write(data); + if (!flushed) { + reverseProxy.incoming.socket.pause(); + proxySocket.once('drain', function () { + try { reverseProxy.incoming.socket.resume() } + catch (er) { console.error("reverseProxy.incoming.socket.resume error: %s", er.message) } + }); + + // + // Force the `drain` event in 100ms if it hasn't + // happened on its own. + // + setTimeout(function () { + proxySocket.emit('drain'); + }, 100); + } + } + catch (ex) { + detach(); + proxySocket.end(); + socket.end(); + } + }); + + // + // Helper function to detach all event listeners + // from `reverseProxy` and `proxySocket`. + // + function detach() { + proxySocket.removeListener('end', listeners.onIncomingClose); + proxySocket.removeListener('data', listeners.onIncoming); + reverseProxy.incoming.socket.removeListener('end', listeners.onOutgoingClose); + reverseProxy.incoming.socket.removeListener('data', listeners.onOutgoing); + } + + // + // If the incoming `proxySocket` socket closes, then + // detach all event listeners. + // + proxySocket.on('end', listeners.onIncomingClose = function() { + reverseProxy.incoming.socket.end(); + detach(); + + // Emit the `end` event now that we have completed proxying + self.emit('websocket:end', req, socket, head); + }); + + // + // If the `reverseProxy` socket closes, then detach all + // event listeners. + // + reverseProxy.incoming.socket.on('end', listeners.onOutgoingClose = function() { + proxySocket.end(); + detach(); + }); + }; + + function getPort (port) { + port = port || 80; + return port - 80 === 0 ? '' : ':' + port + } + + // + // Get the protocol, and host for this request and create an instance + // of `http.Agent` or `https.Agent` from the pool managed by `node-http-proxy`. + // + var agent = this.target.agent, + protocolName = this.target.https ? 'https' : 'http', + portUri = getPort(this.source.port), + remoteHost = this.target.host + portUri; + + // + // Change headers (if requested). + // + if (this.changeOrigin) { + req.headers.host = remoteHost; + req.headers.origin = protocolName + '://' + remoteHost; + } + + // + // Make the outgoing WebSocket request + // + outgoing.host = this.target.host; + outgoing.port = this.target.port; + outgoing.method = 'GET'; + outgoing.path = req.url; + outgoing.headers = req.headers; + + var reverseProxy = agent.appendMessage(outgoing); + + // + // On any errors from the `reverseProxy` emit the + // `webSocketProxyError` and close the appropriate + // connections. + // + function proxyError (err) { + reverseProxy.end(); + if (self.emit('webSocketProxyError', req, socket, head)) { + return; + } + + socket.end(); + } + + // + // Here we set the incoming `req`, `socket` and `head` data to the outgoing + // request so that we can reuse this data later on in the closure scope + // available to the `upgrade` event. This bookkeeping is not tracked anywhere + // in nodejs core and is **very** specific to proxying WebSockets. + // + reverseProxy.agent = agent; + reverseProxy.incoming = { + request: req, + socket: socket, + head: head + }; + + // + // If the agent for this particular `host` and `port` combination + // is not already listening for the `upgrade` event, then do so once. + // This will force us not to disconnect. + // + // In addition, it's important to note the closure scope here. Since + // there is no mapping of the socket to the request bound to it. + // + if (!agent._events || agent._events['upgrade'].length === 0) { + agent.on('upgrade', function (_, remoteSocket, head) { + // + // Prepare the socket for the reverseProxy request and begin to + // stream data between the two sockets. Here it is important to + // note that `remoteSocket._httpMessage === reverseProxy`. + // + _socket(remoteSocket, true); + onUpgrade(remoteSocket._httpMessage, remoteSocket); + }); + } + + // + // If the reverseProxy connection has an underlying socket, + // then execute the WebSocket handshake. + // + if (typeof reverseProxy.socket !== 'undefined') { + reverseProxy.socket.on('data', function handshake (data) { + // + // Ok, kind of harmfull part of code. Socket.IO sends a hash + // at the end of handshake if protocol === 76, but we need + // to replace 'host' and 'origin' in response so we split + // data to printable data and to non-printable. (Non-printable + // will come after double-CRLF). + // + var sdata = data.toString(); + + // Get the Printable data + sdata = sdata.substr(0, sdata.search(CRLF + CRLF)); + + // Get the Non-Printable data + data = data.slice(Buffer.byteLength(sdata), data.length); + + if (self.source.https && !self.target.https) { + // + // If the proxy server is running HTTPS but the client is running + // HTTP then replace `ws` with `wss` in the data sent back to the client. + // + sdata = sdata.replace('ws:', 'wss:'); + } + + try { + // + // Write the printable and non-printable data to the socket + // from the original incoming request. + // + self.emit('websocket:handshake', req, socket, head, sdata, data); + socket.write(sdata); + var flushed = socket.write(data); + if (!flushed) { + reverseProxy.socket.pause(); + socket.once('drain', function () { + try { reverseProxy.socket.resume() } + catch (er) { console.error("reverseProxy.socket.resume error: %s", er.message) } + }); + + // + // Force the `drain` event in 100ms if it hasn't + // happened on its own. + // + setTimeout(function () { + socket.emit('drain'); + }, 100); + } + } + catch (ex) { + // + // Remove data listener on socket error because the + // 'handshake' has failed. + // + reverseProxy.socket.removeListener('data', handshake); + return proxyError(ex); + } + + // Catch socket errors + socket.on('error', proxyError); + + // + // Remove data listener now that the 'handshake' is complete + // + reverseProxy.socket.removeListener('data', handshake); + }); + } + + reverseProxy.on('error', proxyError); + + try { + // + // Attempt to write the upgrade-head to the reverseProxy + // request. This is small, and there's only ever one of + // it; no need for pause/resume. + // + reverseProxy.write(head); + } + catch (ex) { + return proxyError(ex); + } + + // + // If we have been passed buffered data, resume it. + // + if (buffer) { + return !errState + ? buffer.resume() + : buffer.destroy(); + } +}; + +// +// ### function close() +// Closes all sockets associated with the Agents +// belonging to this instance. +// +HttpProxy.prototype.close = function () { + [this.forward, this.target].forEach(function (proxy) { + if (proxy && proxy.agent) { + proxy.agent.sockets.forEach(function (socket) { + socket.end(); + }); + } + }); +}; + +// +// ### @private function _forwardRequest (req) +// #### @req {ServerRequest} Incoming HTTP Request to proxy. +// Forwards the specified `req` to the location specified +// by `this.forward` ignoring errors and the subsequent response. +// +HttpProxy.prototype._forwardRequest = function (req) { + var self = this, + outgoing = new(this.forward.base), + forwardProxy; + + // + // Setup outgoing proxy with relevant properties. + // + outgoing.host = this.forward.host; + outgoing.port = this.forward.port, + outgoing.agent = this.forward.agent; + outgoing.method = req.method; + outgoing.path = req.url; + outgoing.headers = req.headers; + + // + // Open new HTTP request to internal resource with will + // act as a reverse proxy pass. + // + forwardProxy = this.forward.protocol.request(outgoing, function (response) { + // + // Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. + // Remark (indexzero): We will eventually emit a 'forward' event here for performance tuning. + // + }); + + // + // Add a listener for the connection timeout event. + // + // Remark: Ignoring this error in the event + // forward target doesn't exist. + // + forwardProxy.once('error', function (err) { }); + + // + // Chunk the client request body as chunks from + // the proxied request come in + // + req.on('data', function (chunk) { + var flushed = forwardProxy.write(chunk); + if (!flushed) { + req.pause(); + forwardProxy.once('drain', function () { + try { req.resume() } + catch (er) { console.error("req.resume error: %s", er.message) } + }); + + // + // Force the `drain` event in 100ms if it hasn't + // happened on its own. + // + setTimeout(function () { + forwardProxy.emit('drain'); + }, 100); + } + }); + + // + // At the end of the client request, we are going to + // stop the proxied request + // + req.on('end', function () { + forwardProxy.end(); + }); +}; \ No newline at end of file diff --git a/lib/proxy-table.js b/lib/node-http-proxy/proxy-table.js similarity index 78% rename from lib/proxy-table.js rename to lib/node-http-proxy/proxy-table.js index 90a129b..749ea7d 100644 --- a/lib/proxy-table.js +++ b/lib/node-http-proxy/proxy-table.js @@ -1,7 +1,7 @@ /* node-http-proxy.js: Lookup table for proxy targets in node.js - Copyright (c) 2010 Charlie Robbins + Copyright (c) 2010 Charlie Robbins Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -29,7 +29,7 @@ var util = require('util'), fs = require('fs'); // -// ### function ProxyTable (router, silent) +// ### function ProxyTable (router, silent) // #### @router {Object} Object containing the host based routes // #### @silent {Boolean} Value indicating whether we should suppress logs // #### @hostnameOnly {Boolean} Value indicating if we should route based on __hostname string only__ @@ -37,31 +37,34 @@ var util = require('util'), // locations of proxy targets based on ServerRequest headers; specifically // the HTTP host header. // -var ProxyTable = exports.ProxyTable = function (router, silent, hostnameOnly) { +var ProxyTable = exports.ProxyTable = function (options) { events.EventEmitter.call(this); - - this.silent = typeof silent !== 'undefined' ? silent : true; - this.hostnameOnly = typeof hostnameOnly !== 'undefined' ? hostnameOnly : false; - - if (typeof router === 'object') { + + this.silent = options.silent || options.silent !== true; + this.hostnameOnly = options.hostnameOnly === true; + + if (typeof options.router === 'object') { // - // If we are passed an object literal setup - // the routes with RegExps from the router + // If we are passed an object literal setup + // the routes with RegExps from the router // - this.setRoutes(router); + this.setRoutes(options.router); } - else if (typeof router === 'string') { + else if (typeof options.router === 'string') { // - // If we are passed a string then assume it is a + // If we are passed a string then assume it is a // file path, parse that file and watch it for changes // var self = this; - this.routeFile = router; - this.setRoutes(JSON.parse(fs.readFileSync(router)).router); - + this.routeFile = options.router; + this.setRoutes(JSON.parse(fs.readFileSync(options.router)).router); + fs.watchFile(this.routeFile, function () { fs.readFile(self.routeFile, function (err, data) { - if (err) throw err; + if (err) { + self.emit('error', err); + } + self.setRoutes(JSON.parse(data).router); self.emit('routes', self.hostnameOnly === false ? self.routes : self.router); }); @@ -72,19 +75,23 @@ var ProxyTable = exports.ProxyTable = function (router, silent, hostnameOnly) { } }; -// Inherit from events.EventEmitter +// +// Inherit from `events.EventEmitter` +// util.inherits(ProxyTable, events.EventEmitter); // -// ### function setRoutes (router) +// ### function setRoutes (router) // #### @router {Object} Object containing the host based routes -// Sets the host-based routes to be used by this instance. +// Sets the host-based routes to be used by this instance. // ProxyTable.prototype.setRoutes = function (router) { - if (!router) throw new Error('Cannot update ProxyTable routes without router.'); - + if (!router) { + throw new Error('Cannot update ProxyTable routes without router.'); + } + this.router = router; - + if (this.hostnameOnly === false) { var self = this; this.routes = []; @@ -101,7 +108,7 @@ ProxyTable.prototype.setRoutes = function (router) { }; // -// ### function getProxyLocation (req) +// ### function getProxyLocation (req) // #### @req {ServerRequest} The incoming server request to get proxy information about. // Returns the proxy location based on the HTTP Headers in the ServerRequest `req` // available to this instance. @@ -110,14 +117,14 @@ ProxyTable.prototype.getProxyLocation = function (req) { if (!req || !req.headers || !req.headers.host) { return null; } - + var target = req.headers.host.split(':')[0]; if (this.hostnameOnly == true) { if (this.router.hasOwnProperty(target)) { var location = this.router[target].split(':'), host = location[0], port = location.length === 1 ? 80 : location[1]; - + return { port: port, host: host @@ -127,7 +134,9 @@ ProxyTable.prototype.getProxyLocation = function (req) { else { target += req.url; for (var i in this.routes) { - var match, route = this.routes[i]; + var route = this.routes[i], + match; + if (match = target.match(route.route)) { var location = route.target.split(':'), host = location[0], @@ -140,7 +149,7 @@ ProxyTable.prototype.getProxyLocation = function (req) { } } } - + return null; }; diff --git a/lib/node-http-proxy/routing-proxy.js b/lib/node-http-proxy/routing-proxy.js new file mode 100644 index 0000000..3938b26 --- /dev/null +++ b/lib/node-http-proxy/routing-proxy.js @@ -0,0 +1,247 @@ +/* + * routing-proxy.js: A routing proxy consuming a RoutingTable and multiple HttpProxy instances + * + * (C) 2011 Nodejitsu Inc. + * MIT LICENCE + * + */ + +var events = require('events'), + util = require('util'), + HttpProxy = require('./http-proxy').HttpProxy, + ProxyTable = require('./proxy-table').ProxyTable; + +// +// ### function RoutingProxy (options) +// #### @options {Object} Options for this instance +// Constructor function for the RoutingProxy object, a higher level +// reverse proxy Object which can proxy to multiple hosts and also interface +// easily with a RoutingTable instance. +// +var RoutingProxy = exports.RoutingProxy = function (options) { + events.EventEmitter.call(this); + + var self = this; + options = options || {}; + + if (options.router) { + this.proxyTable = new ProxyTable(options); + this.proxyTable.on('routes', function (routes) { + self.emit('routes', routes); + }); + } + + // + // Create a set of `HttpProxy` objects to be used later on calls + // to `.proxyRequest()` and `.proxyWebSocketRequest()`. + // + this.proxies = {}; + + // + // Setup default target options (such as `https`). + // + this.targetĀ  = {}; + this.target.https = options.target && options.target.https; + + // + // Setup other default options to be used for instances of + // `HttpProxy` created by this `RoutingProxy` instance. + // + this.source = options.source || { host: 'localhost', port: 8000 }; + this.https = this.source.https || options.https; + this.enable = options.enable; + this.forward = options.forward; +}; + + +// +// Inherit from `events.EventEmitter`. +// +util.inherits(RoutingProxy, events.EventEmitter); + +// +// ### function add (options) +// #### @options {Object} Options for the `HttpProxy` to add. +// Adds a new instance of `HttpProxy` to this `RoutingProxy` instance +// for the specified `options.host` and `options.port`. +// +RoutingProxy.prototype.add = function (options) { + var self = this, + key = this._getKey(options); + + // + // TODO: Consume properties in `options` related to the `ProxyTable`. + // + options.target = options.target || {}; + options.target.host = options.target.host || options.host; + options.target.port = options.target.port || options.port; + options.target.https = this.target && this.target.https || + options.target && options.target.https || + options.https; + + // + // Setup options to pass-thru to the new `HttpProxy` instance + // for the specified `options.host` and `options.port` pair. + // + ['https', 'enable', 'forward'].forEach(function (key) { + if (options[key] !== false && self[key]) { + options[key] = self[key]; + } + }); + + this.proxies[key] = new HttpProxy(options); +}; + +// +// ### function remove (options) +// #### @options {Object} Options mapping to the `HttpProxy` to remove. +// Removes an instance of `HttpProxy` from this `RoutingProxy` instance +// for the specified `options.host` and `options.port` (if they exist). +// +RoutingProxy.prototype.remove = function (options) { + var key = this._getKey(options); +}; + +// +// ### function close() +// Cleans up any state left behind (sockets, timeouts, etc) +// associated with this instance. +// +RoutingProxy.prototype.close = function () { + var self = this; + + if (this.proxyTable) { + // + // Close the `RoutingTable` associated with + // this instance (if any). + // + this.proxyTable.close(); + } + + // + // Close all sockets for all `HttpProxy` object(s) + // associated with this instance. + // + Object.keys(this.proxies).forEach(function (key) { + self.proxies[key].close(); + }); +}; + +// +// ### function proxyRequest (req, res, [port, host, paused]) +// #### @req {ServerRequest} Incoming HTTP Request to proxy. +// #### @res {ServerResponse} Outgoing HTTP Request to write proxied data to. +// #### @options {Object} Options for the outgoing proxy request. +// +// options.port {number} Port to use on the proxy target host. +// options.host {string} Host of the proxy target. +// options.buffer {Object} Result from `httpProxy.buffer(req)` +// options.https {Object|boolean} Settings for https. +// +RoutingProxy.prototype.proxyRequest = function (req, res, options) { + options = options || {}; + + // + // Check the proxy table for this instance to see if we need + // to get the proxy location for the request supplied. We will + // always ignore the proxyTable if an explicit `port` and `host` + // arguments are supplied to `proxyRequest`. + // + if (this.proxyTable && !options.host) { + location = this.proxyTable.getProxyLocation(req); + + // + // If no location is returned from the ProxyTable instance + // then respond with `404` since we do not have a valid proxy target. + // + if (!location) { + try { + res.writeHead(404); + res.end(); + } + catch (er) { + console.error("res.writeHead/res.end error: %s", er.message); + } + + return; + } + + // + // When using the ProxyTable in conjunction with an HttpProxy instance + // only the following arguments are valid: + // + // * `proxy.proxyRequest(req, res, { host: 'localhost' })`: This will be skipped + // * `proxy.proxyRequest(req, res, { buffer: buffer })`: Buffer will get updated appropriately + // * `proxy.proxyRequest(req, res)`: Options will be assigned appropriately. + // + options.port = location.port; + options.host = location.host; + } + + var key = options.host + ':' + options.port, + proxy; + + if (!this.proxies[key]) { + this.add(options); + } + + proxy = this.proxies[key]; + proxy.proxyRequest(req, res, options.buffer); +}; + +// +// ### function proxyWebSocketRequest (req, socket, head, options) +// #### @req {ServerRequest} Websocket request to proxy. +// #### @socket {net.Socket} Socket for the underlying HTTP request +// #### @head {string} Headers for the Websocket request. +// #### @options {Object} Options to use when proxying this request. +// +// options.port {number} Port to use on the proxy target host. +// options.host {string} Host of the proxy target. +// options.buffer {Object} Result from `httpProxy.buffer(req)` +// options.https {Object|boolean} Settings for https. +// +RoutingProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options) { + options = options || {}; + + if (this.proxyTable && !options.host) { + location = this.proxyTable.getProxyLocation(req); + + if (!location) { + return socket.destroy(); + } + + options.port = location.port; + options.host = location.host; + } + + var key = options.host + ':' + options.port, + proxy; + + if (!this.proxies[key]) { + this.add(options); + } + + proxy = this.proxies[key]; + proxy.proxyWebSocketRequest(req, socket, head, options.buffer); +}; + +// +// ### @private function _getKey (options) +// #### @options {Object} Options to extract the key from +// Ensures that the appropriate options are present in the `options` +// provided and responds with a string key representing the `host`, `port` +// combination contained within. +// +RoutingProxy.prototype._getKey = function (options) { + if (!options || ((!options.host || !options.port) + && (!options.target || !options.target.host || !options.target.port))) { + throw new Error('options.host and options.port or options.target are required.'); + return; + } + + return [ + options.host || options.target.host, + options.port || options.target.port + ].join(':'); +} \ No newline at end of file diff --git a/package.json b/package.json index 846c688..0aca902 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "http-proxy", "description": "A full-featured http reverse proxy for node.js", - "version": "0.6.6", + "version": "0.7.0", "author": "Charlie Robbins ", "contributors": [ { "name": "Mikeal Rogers", "email": "mikeal.rogers@gmail.com" }, { "name": "Marak Squires", "email": "marak.squires@gmail.com" }, - { "name": "Fedor Indutny", "email": "fedor.indutny@gmail.com" } + { "name": "Fedor Indutny", "email": "fedor.indutny@gmail.com" }, + { "name": "Dominic Tarr", "email": "dominic@nodejitsu.com" } ], "repository": { "type": "git", @@ -25,6 +26,10 @@ }, "main": "./lib/node-http-proxy", "bin": { "node-http-proxy": "./bin/node-http-proxy" }, - "scripts": { "test": "vows test/*-test.js --spec && vows test/*-test.js --spec --https" }, + "scripts": { + "test": "npm run-script test-http && npm run-script test-https", + "test-http": "vows --spec && vows --spec --target=secure", + "test-https": "vows --spec --source=secure && vows --spec --source=secure --target=secure" + }, "engines": { "node": "0.4.x || 0.5.x" } } diff --git a/test/helpers.js b/test/helpers.js index 23a29e6..b1bbd22 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -5,27 +5,16 @@ * */ -var fs = require('fs'), +var assert = require('assert'), + fs = require('fs'), http = require('http'), https = require('https'), path = require('path'), - vows = require('vows'), - assert = require('assert'), + argv = require('optimist').argv, request = require('request'), - websocket = require('./../vendor/websocket'), - httpProxy = require('./../lib/node-http-proxy'); - -function merge (target) { - var objs = Array.prototype.slice.call(arguments, 1); - objs.forEach(function(o) { - Object.keys(o).forEach(function (attr) { - if (! o.__lookupGetter__(attr)) { - target[attr] = o[attr]; - } - }); - }); - return target; -} + vows = require('vows'), + websocket = require('../vendor/websocket'), + httpProxy = require('../lib/node-http-proxy'); var loadHttps = exports.loadHttps = function () { return { @@ -34,21 +23,35 @@ var loadHttps = exports.loadHttps = function () { }; }; -var TestRunner = exports.TestRunner = function (protocol, target) { - this.options = {}; - this.options.target = {}; - this.protocol = protocol; - this.target = target; - this.testServers = []; - - if (protocol === 'https') { - this.options.https = loadHttps(); +var parseProtocol = exports.parseProtocol = function () { + function setupProtocol (secure) { + return { + secure: secure, + protocols: { + http: secure ? 'https' : 'http', + ws: secure ? 'wss' : 'ws' + } + } } - if (target === 'https') { - this.options.target = { - https: loadHttps() - }; + return { + source: setupProtocol(argv.source === 'secure'), + target: setupProtocol(argv.target === 'secure') + }; +} + +var TestRunner = exports.TestRunner = function (options) { + options = options || {}; + this.source = options.source || {}; + this.target = options.target || {}; + this.testServers = []; + + if (this.source.secure) { + this.source.https = loadHttps(); + } + + if (this.target.secure) { + this.target.https = loadHttps(); } }; @@ -59,13 +62,17 @@ TestRunner.prototype.assertProxied = function (host, proxyPort, port, createProx var test = { topic: function () { - var that = this, options = { + var that = this, + options; + + options = { method: 'GET', - uri: self.protocol + '://localhost:' + proxyPort, + uri: self.source.protocols.http + '://localhost:' + proxyPort, headers: { host: host } }; + function startTest () { if (port) { @@ -91,7 +98,7 @@ TestRunner.prototype.assertProxied = function (host, proxyPort, port, createProx TestRunner.prototype.assertResponseCode = function (proxyPort, statusCode, createProxy) { var assertion = "should receive " + statusCode + " responseCode", - protocol = this.protocol; + protocol = this.source.protocols.http; var test = { topic: function () { @@ -124,7 +131,6 @@ TestRunner.prototype.assertResponseCode = function (proxyPort, statusCode, creat // // WebSocketTest // - TestRunner.prototype.webSocketTest = function (options) { var self = this; @@ -161,7 +167,6 @@ TestRunner.prototype.webSocketTest = function (options) { // // WebSocketTestWithTable // - TestRunner.prototype.webSocketTestWithTable = function (options) { var self = this; @@ -172,9 +177,9 @@ TestRunner.prototype.webSocketTestWithTable = function (options) { options.onListen(socket); } - self.startProxyServerWithTable( - options.ports.proxy, - {router: options.router}, + self.startProxyServerWithTable( + options.ports.proxy, + { router: options.router }, function (err, proxy) { if (options.onServer) { options.onServer(proxy) } @@ -199,8 +204,7 @@ TestRunner.prototype.webSocketTestWithTable = function (options) { // TestRunner.prototype.startProxyServer = function (port, targetPort, host, callback) { var that = this, - options = that.options, - proxyServer = httpProxy.createServer(targetPort, host, options); + proxyServer = httpProxy.createServer(host, targetPort, this.getOptions()); proxyServer.listen(port, function () { that.testServers.push(proxyServer); @@ -212,18 +216,19 @@ TestRunner.prototype.startProxyServer = function (port, targetPort, host, callba // Creates the reverse proxy server with a specified latency // TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, latency, callback) { + // // Initialize the nodeProxy and start proxying the request - var that = this, proxyServer = httpProxy.createServer(function (req, res, proxy) { - var buffer = proxy.buffer(req); + // + var that = this, + proxyServer; + + proxyServer = httpProxy.createServer(host, targetPort, function (req, res, proxy) { + var buffer = httpProxy.buffer(req); setTimeout(function () { - proxy.proxyRequest(req, res, { - port: targetPort, - host: host, - buffer: buffer - }); + proxy.proxyRequest(req, res, buffer); }, latency); - }, this.options); + }, this.getOptions()); proxyServer.listen(port, function () { that.testServers.push(proxyServer); @@ -235,7 +240,9 @@ TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, // Creates the reverse proxy server with a ProxyTable // TestRunner.prototype.startProxyServerWithTable = function (port, options, callback) { - var that = this, proxyServer = httpProxy.createServer(merge({}, options, this.options)); + var that = this, + proxyServer = httpProxy.createServer(merge({}, options, this.getOptions())); + proxyServer.listen(port, function () { that.testServers.push(proxyServer); callback(); @@ -248,13 +255,15 @@ TestRunner.prototype.startProxyServerWithTable = function (port, options, callba // Creates a latent reverse proxy server using a ProxyTable // TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, latency, options, callback) { + // // Initialize the nodeProxy and start proxying the request - var proxyServer, - that = this, - proxy = new httpProxy.HttpProxy(merge({}, options, that.options)); + // + var that = this, + proxy = new httpProxy.RoutingProxy(merge({}, options, this.getOptions())), + proxyServer; var handler = function (req, res) { - var buffer = proxy.buffer(req); + var buffer = httpProxy.buffer(req); setTimeout(function () { proxy.proxyRequest(req, res, { buffer: buffer @@ -262,9 +271,9 @@ TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, laten }, latency); }; - proxyServer = that.options.https - ? https.createServer(that.options.https, handler, that.options) - : http.createServer(handler, that.options); + proxyServer = this.source.https + ? https.createServer(this.source.https, handler) + : http.createServer(handler); proxyServer.listen(port, function () { that.testServers.push(proxyServer); @@ -278,7 +287,9 @@ TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, laten // Creates proxy server forwarding to the specified options // TestRunner.prototype.startProxyServerWithForwarding = function (port, targetPort, host, options, callback) { - var that = this, proxyServer = httpProxy.createServer(targetPort, host, merge({}, options, this.options)); + var that = this, + proxyServer = httpProxy.createServer(targetPort, host, merge({}, options, this.getOptions())); + proxyServer.listen(port, function () { that.testServers.push(proxyServer); callback(null, proxyServer); @@ -289,14 +300,18 @@ TestRunner.prototype.startProxyServerWithForwarding = function (port, targetPort // Creates the 'hellonode' server // TestRunner.prototype.startTargetServer = function (port, output, callback) { - var that = this, targetServer, handler = function (req, res) { + var that = this, + targetServer, + handler; + + handler = function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write(output); res.end(); }; - - targetServer = this.options.target.https - ? https.createServer(this.options.target.https, handler) + + targetServer = this.target.https + ? https.createServer(this.target.https, handler) : http.createServer(handler); targetServer.listen(port, function () { @@ -315,3 +330,42 @@ TestRunner.prototype.closeServers = function () { return this.testServers; }; + +// +// Creates a new instance of the options to +// pass to `httpProxy.createServer()` +// +TestRunner.prototype.getOptions = function () { + return { + https: clone(this.source.https), + target: { + https: clone(this.target.https) + } + }; +}; + +// +// ### @private function clone (object) +// #### @object {Object} Object to clone +// Shallow clones the specified object. +// +function clone (object) { + if (!object) { return null } + + return Object.keys(object).reduce(function (obj, k) { + obj[k] = object[k]; + return obj; + }, {}); +} + +function merge (target) { + var objs = Array.prototype.slice.call(arguments, 1); + objs.forEach(function(o) { + Object.keys(o).forEach(function (attr) { + if (! o.__lookupGetter__(attr)) { + target[attr] = o[attr]; + } + }); + }); + return target; +} \ No newline at end of file diff --git a/test/node-http-proxy-test.js b/test/http/http-proxy-test.js similarity index 91% rename from test/node-http-proxy-test.js rename to test/http/http-proxy-test.js index f05ae5a..c202f97 100644 --- a/test/node-http-proxy-test.js +++ b/test/http/http-proxy-test.js @@ -26,10 +26,9 @@ var assert = require('assert'), util = require('util'), - argv = require('optimist').argv, request = require('request'), vows = require('vows'), - helpers = require('./helpers'); + helpers = require('../helpers'); var forwardOptions = { forward: { @@ -45,11 +44,11 @@ var badForwardOptions = { } }; -var protocol = argv.https ? 'https' : 'http', - target = argv.target ? argv.target : 'http', - runner = new helpers.TestRunner(protocol, target); +var options = helpers.parseProtocol(), + testName = [options.source.protocols.http, options.target.protocols.http].join('-to-'), + runner = new helpers.TestRunner(options); -vows.describe('node-http-proxy/' + protocol).addBatch({ +vows.describe('node-http-proxy/http-proxy/' + testName).addBatch({ "When using server created by httpProxy.createServer()": { "with no latency" : { "and a valid target server": runner.assertProxied('localhost', 8080, 8081, function (callback) { diff --git a/test/proxy-table-test.js b/test/http/routing-proxy-test.js similarity index 87% rename from test/proxy-table-test.js rename to test/http/routing-proxy-test.js index 359c82c..1180fb7 100644 --- a/test/proxy-table-test.js +++ b/test/http/routing-proxy-test.js @@ -12,11 +12,11 @@ var assert = require('assert'), argv = require('optimist').argv, request = require('request'), vows = require('vows'), - helpers = require('./helpers'), - TestRunner = helpers.TestRunner; + helpers = require('../helpers'); -var protocol = argv.https ? 'https' : 'http', - runner = new TestRunner(protocol), +var options = helpers.parseProtocol(), + testName = [options.source.protocols.http, options.target.protocols.http].join('-to-'), + runner = new helpers.TestRunner(options), routeFile = path.join(__dirname, 'config.json'); var fileOptions = { @@ -41,7 +41,7 @@ var hostnameOptions = { } }; -vows.describe('node-http-proxy/proxy-table/' + protocol).addBatch({ +vows.describe('node-http-proxy/routing-proxy/' + testName).addBatch({ "When using server created by httpProxy.createServer()": { "when passed a routing table": { "and routing by RegExp": { @@ -82,16 +82,14 @@ vows.describe('node-http-proxy/proxy-table/' + protocol).addBatch({ fs.writeFileSync(routeFile, JSON.stringify(config)); this.server.on('routes', function () { - var options = { - method: 'GET', - uri: protocol + '://localhost:8100', - headers: { - host: 'dynamic.com' - } - }; - runner.startTargetServer(8103, 'hello dynamic.com', function () { - request(options, that.callback); + request({ + method: 'GET', + uri: options.source.protocols.http + '://localhost:8100', + headers: { + host: 'dynamic.com' + } + }, that.callback); }); }); }, diff --git a/test/web-socket-proxy-test.js b/test/websocket/websocket-proxy-test.js similarity index 72% rename from test/web-socket-proxy-test.js rename to test/websocket/websocket-proxy-test.js index 339bc71..7d833f5 100644 --- a/test/web-socket-proxy-test.js +++ b/test/websocket/websocket-proxy-test.js @@ -30,8 +30,8 @@ var util = require('util'), colors = require('colors'), request = require('request'), vows = require('vows'), - websocket = require('./../vendor/websocket'), - helpers = require('./helpers'); + websocket = require('../../vendor/websocket'), + helpers = require('../helpers'); try { var utils = require('socket.io/lib/socket.io/utils'), @@ -43,55 +43,11 @@ catch (ex) { process.exit(1); } -var protocol = argv.https ? 'https' : 'http', - wsprotocol = argv.https ? 'wss' : 'ws', - runner = new helpers.TestRunner(protocol); +var options = helpers.parseProtocol(), + testName = [options.source.protocols.ws, options.target.protocols.ws].join('-to-'), + runner = new helpers.TestRunner(options); -vows.describe('node-http-proxy/websocket/' + wsprotocol).addBatch({ - "when using proxy table":{ - "with no latency" : { - "when an inbound message is sent from a WebSocket client": { - topic: function () { - var that = this - headers = {}; - - runner.webSocketTestWithTable({ - io: io, - host: 'localhost', - wsprotocol: wsprotocol, - protocol: protocol, - router: {'localhost':'localhost:8230'}, - ports: { - target: 8230, - proxy: 8231 - }, - onListen: function (socket) { - socket.on('connection', function (client) { - client.on('message', function (msg) { - that.callback(null, msg, headers); - }); - }); - }, - onWsupgrade: function (req, res) { - headers.request = req; - headers.response = res.headers; - }, - onOpen: function (ws) { - ws.send(utils.encode('from client')); - } - }); - }, - "the target server should receive the message": function (err, msg, headers) { - assert.equal(msg, 'from client'); - }, - "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { - assert.isString(headers.response['sec-websocket-location']); - assert.isTrue(headers.response['sec-websocket-location'].indexOf(wsprotocol) !== -1); - assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); - } - } - } - }, +vows.describe('node-http-proxy/http-proxy/' + testName).addBatch({ "When using server created by httpProxy.createServer()": { "with no latency" : { "when an inbound message is sent from a WebSocket client": { @@ -102,8 +58,8 @@ vows.describe('node-http-proxy/websocket/' + wsprotocol).addBatch({ runner.webSocketTest({ io: io, host: 'localhost', - wsprotocol: wsprotocol, - protocol: protocol, + wsprotocol: options.source.protocols.ws, + protocol: options.source.protocols.http, ports: { target: 8130, proxy: 8131 @@ -129,7 +85,7 @@ vows.describe('node-http-proxy/websocket/' + wsprotocol).addBatch({ }, "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { assert.isString(headers.response['sec-websocket-location']); - assert.isTrue(headers.response['sec-websocket-location'].indexOf(wsprotocol) !== -1); + assert.isTrue(headers.response['sec-websocket-location'].indexOf(options.source.protocols.ws) !== -1); assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); } }, @@ -141,8 +97,8 @@ vows.describe('node-http-proxy/websocket/' + wsprotocol).addBatch({ runner.webSocketTest({ io: io, host: 'localhost', - wsprotocol: wsprotocol, - protocol: protocol, + wsprotocol: options.source.protocols.ws, + protocol: options.source.protocols.http, ports: { target: 8132, proxy: 8133 @@ -170,8 +126,8 @@ vows.describe('node-http-proxy/websocket/' + wsprotocol).addBatch({ runner.webSocketTest({ io: io, host: 'localhost', - wsprotocol: wsprotocol, - protocol: protocol, + wsprotocol: options.source.protocols.ws, + protocol: options.source.protocols.http, ports: { target: 8134, proxy: 8135 @@ -198,7 +154,7 @@ vows.describe('node-http-proxy/websocket/' + wsprotocol).addBatch({ }, "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { assert.isString(headers.response['sec-websocket-location']); - assert.isTrue(headers.response['sec-websocket-location'].indexOf(wsprotocol) !== -1); + assert.isTrue(headers.response['sec-websocket-location'].indexOf(options.source.protocols.ws) !== -1); assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); } } diff --git a/test/websocket/websocket-routing-proxy-test.js b/test/websocket/websocket-routing-proxy-test.js new file mode 100644 index 0000000..2e5addf --- /dev/null +++ b/test/websocket/websocket-routing-proxy-test.js @@ -0,0 +1,104 @@ +/* + node-http-proxy-test.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Marak Squires and Fedor Indutny + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +var util = require('util'), + assert = require('assert'), + argv = require('optimist').argv, + colors = require('colors'), + request = require('request'), + vows = require('vows'), + websocket = require('../../vendor/websocket'), + helpers = require('../helpers'); + +try { + var utils = require('socket.io/lib/socket.io/utils'), + io = require('socket.io'); +} +catch (ex) { + console.error('Socket.io is required for this example:'); + console.error('npm ' + 'install'.green + ' socket.io@0.6.18'.magenta); + process.exit(1); +} + +var options = helpers.parseProtocol(), + testName = [options.source.protocols.ws, options.target.protocols.ws].join('-to-'), + runner = new helpers.TestRunner(options); + +vows.describe('node-http-proxy/routing-proxy/' + testName).addBatch({ + "When using server created by httpProxy.createServer()": { + "using proxy table with no latency": { + "when an inbound message is sent from a WebSocket client": { + topic: function () { + var that = this + headers = {}; + + runner.webSocketTestWithTable({ + io: io, + host: 'localhost', + wsprotocol: options.source.protocols.ws, + protocol: options.source.protocols.http, + router: { 'localhost' : 'localhost:8230' }, + ports: { + target: 8230, + proxy: 8231 + }, + onListen: function (socket) { + socket.on('connection', function (client) { + client.on('message', function (msg) { + that.callback(null, msg, headers); + }); + }); + }, + onWsupgrade: function (req, res) { + headers.request = req; + headers.response = res.headers; + }, + onOpen: function (ws) { + ws.send(utils.encode('from client')); + } + }); + }, + "the target server should receive the message": function (err, msg, headers) { + assert.equal(msg, 'from client'); + }, + "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { + assert.isString(headers.response['sec-websocket-location']); + assert.isTrue(headers.response['sec-websocket-location'].indexOf(options.source.protocols.ws) !== -1); + assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); + } + } + } + } +}).addBatch({ + "When the tests are over": { + topic: function () { + return runner.closeServers(); + }, + "the servers should clean up": function () { + assert.isTrue(true); + } + } +}).export(module);