/* * routing-proxy.js: A routing proxy consuming a RoutingTable and multiple HttpProxy instances * * (C) 2011 Nodejitsu Inc. * MIT LICENCE * */ var events = require('events'), utile = require('utile'), 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; this.target.maxSockets = options.target && options.target.maxSockets; // // 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; this.changeOrigin = options.changeOrigin || false; // // Listen for 'newListener' events so that we can bind 'proxyError' // listeners to each HttpProxy's 'proxyError' event. // this.on('newListener', function (evt) { if (evt === 'proxyError' || evt === 'webSocketProxyError') { Object.keys(self.proxies).forEach(function (key) { self.proxies[key].on(evt, self.emit.bind(self, evt)); }); } }); }; // // Inherit from `events.EventEmitter`. // utile.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.socketPath = options.target.socketPath || options.socketPath; options.target.https = this.target && this.target.https || options.target && options.target.https; options.target.maxSockets = this.target && this.target.maxSockets; // // Setup options to pass-thru to the new `HttpProxy` instance // for the specified `options.host` and `options.port` pair. // ['https', 'enable', 'forward', 'changeOrigin'].forEach(function (key) { if (options[key] !== false && self[key]) { options[key] = self[key]; } }); this.proxies[key] = new HttpProxy(options); if (this.listeners('proxyError').length > 0) { this.proxies[key].on('proxyError', this.emit.bind(this, 'proxyError')); } if (this.listeners('webSocketProxyError').length > 0) { this.proxies[key].on('webSocketProxyError', this.emit.bind(this, 'webSocketProxyError')); } [ 'start', 'forward', 'end', 'proxyResponse', 'websocket:start', 'websocket:end', 'websocket:incoming', 'websocket:outgoing' ].forEach(function (event) { this.proxies[key].on(event, this.emit.bind(this, event)); }, this); }; // // ### 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), proxy = this.proxies[key]; delete this.proxies[key]; return proxy; }; // // ### 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 || {}; var location; // // 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 { if (!this.emit('notFound', req, res)) { 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 = this._getKey(options), proxy; if ((this.target && this.target.https) || (location && location.protocol === 'https')) { options.target = options.target || {}; options.target.https = true; } if (!this.proxies[key]) { this.add(utile.clone(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 || {}; var location, proxy, key; if (this.proxyTable && !options.host) { location = this.proxyTable.getProxyLocation(req); if (!location) { return socket.destroy(); } options.port = location.port; options.host = location.host; } key = this._getKey(options); if (!this.proxies[key]) { this.add(utile.clone(options)); } proxy = this.proxies[key]; proxy.proxyWebSocketRequest(req, socket, head, options.buffer); }; // // ### function addHost (host, target) // #### @host {String} Host to add to proxyTable // #### @target {String} Target to add to proxyTable // Adds a host to proxyTable // RoutingProxy.prototype.addHost = function (host, target) { if (this.proxyTable) { this.proxyTable.addRoute(host, target); } }; // // ### function removeHost (host) // #### @host {String} Host to remove from proxyTable // Removes a host to proxyTable // RoutingProxy.prototype.removeHost = function (host) { if (this.proxyTable) { this.proxyTable.removeRoute(host); } }; // // ### @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 [ options.host || options.target.host, options.port || options.target.port ].join(':'); };