diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index e0a27e0c8..825d053bb 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,110 +36,11 @@ 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 = {}; - -// -// ### 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 _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]; -} - -// -// ### 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; - - if (typeof secure === 'object') { - outgoing = outgoing || {}; - ['ca', 'cert', 'key'].forEach(function (prop) { - if (secure[prop]) { - outgoing[prop] = secure[prop]; - } - }) - } - - return protocol; -} - -// -// ### function getMaxSockets () -// Returns the maximum number of sockets -// allowed on __every__ outgoing request -// made by __all__ instances of `HttpProxy` -// -exports.getMaxSockets = function () { - return maxSockets; -}; - -// -// ### function setMaxSockets () -// Sets the maximum number of sockets -// allowed on __every__ outgoing request -// made by __all__ instances of `HttpProxy` -// -exports.setMaxSockets = function (value) { - maxSockets = value; -}; - -// -// ### function stack (middlewares, proxy) -// adapted from https://github.com/creationix/stack -// -exports.stack = function stack (middlewares, proxy) { - var handle; - middlewares.reverse().forEach(function (layer) { - var child = handle; - handle = function (req, res) { - var next = function (err) { - if (err) { - throw err; - // - // TODO: figure out where to send errors. - // return error(req, res, err); - // - } - child(req, res); - } - - next.__proto__ = proxy; - layer(req, res, next); - }; - }); - - return handle; -} +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 createServer ([port, host, options, handler]) @@ -255,59 +155,6 @@ exports.createServer = function () { return server; }; -// -// ### 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 @@ -328,8 +175,10 @@ util.inherits(HttpProxy, events.EventEmitter); // 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 = []; +exports.buffer = function (obj) { + var events = [], + onData, + onEnd; obj.on('data', onData = function (data, encoding) { events.push(['data', data, encoding]); @@ -354,567 +203,116 @@ HttpProxy.prototype.buffer = function (obj) { }; // -// ### function close () -// Frees the resources associated with this instance, -// if they exist. +// ### function getMaxSockets () +// Returns the maximum number of sockets +// allowed on __every__ outgoing request +// made by __all__ instances of `HttpProxy` // -HttpProxy.prototype.close = function () { - if (this.proxyTable) { - this.proxyTable.close(); - } +exports.getMaxSockets = function () { + return maxSockets; }; // -// ### 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. +// ### function setMaxSockets () +// Sets the maximum number of sockets +// allowed on __every__ outgoing request +// made by __all__ instances of `HttpProxy` // -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) { - res.writeHead(404); - return res.end(); - } - - // - // 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.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'] = res.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)); - } - } - - res.end(); - } - - 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) { - return res.end(); - } - - // 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 { - res.write(chunk); - } catch (er) { - try { - res.end(); - } catch (er) {} - } - } - }); - - // 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); - res.end(); - - // 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) { - reverseProxy.write(chunk); - } - }); - - // - // 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 && !errState) { - options.buffer.resume(); - } +exports.setMaxSockets = function (value) { + maxSockets = value; }; // -// ### @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. +// ### function stack (middlewares, proxy) +// adapted from https://github.com/creationix/stack // -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); +exports.stack = function stack (middlewares, proxy) { + var handle; + middlewares.reverse().forEach(function (layer) { + var child = handle; + handle = function (req, res) { + var next = function (err) { + if (err) { + throw err; + // + // TODO: figure out where to send errors. + // return error(req, res, err); + // + } + child(req, res); + } - // 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. - // + next.__proto__ = proxy; + layer(req, res, next); + }; }); - // 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) { - forwardProxy.write(chunk); - }) - - // At the end of the client request, we are going to stop the proxied request - req.on('end', function () { - forwardProxy.end(); - }); + return handle; }; // -// ### 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. +// ### function _getAgent (host, port, secure) +// #### @options {Object} Options to use when creating the agent. // -// 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. +// { +// host: 'localhost', +// port: 9000, +// https: true, +// maxSockets: 100 +// } // -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; +// 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.'); } - - // - // 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'); - } + + if (!options.port) { + options.port = options.https ? 443 : 80; } - // - // On `upgrade` from the Agent socket, listen to - // the appropriate events. - // - function onUpgrade (reverseProxy, proxySocket) { - if (!reverseProxy) { - proxySocket.end(); - socket.end(); - return; - } + var Agent = options.https ? https.Agent : http.Agent, + agent; - // - // 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); - reverseProxy.incoming.socket.write(data); - } - catch (e) { - 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); - proxySocket.write(data); - } - catch (e) { - 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(); - }); - }; + agent = new Agent({ + host: options.host, + port: options.port + }); - // Setup the incoming client socket. - _socket(socket); + agent.maxSockets = options.maxSockets || maxSockets; - function getPort (port) { - port = port || 80; - return port - 80 === 0 ? '' : ':' + port - } + return agent; +} - // - // 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; - } +// +// ### 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. +// +exports._getProtocol = function _getProtocol (options) { + return options.https ? https : http; +}; - // - // Make the outgoing WebSocket request - // - outgoing = { - host: options.host, - port: options.port, - method: 'GET', - path: req.url, - headers: req.headers, - }; +// +// ### 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. +// +exports._getBase = function _getBase (options) { + var result = {}; - 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 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); - socket.write(data); + if (typeof options.https === 'object') { + ['ca', 'cert', 'key'].forEach(function (key) { + if (options.https[key]) { + result[key] = options.https[key]; } - 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. - // - reverseProxy.write(head); - } - catch (ex) { - proxyError(ex); - } - - // - // If we have been passed buffered data, resume it. - // - if (options.buffer && !errState) { - options.buffer.resume(); - } -}; + + 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 000000000..9461f2a2b --- /dev/null +++ b/lib/node-http-proxy/http-proxy.js @@ -0,0 +1,624 @@ +/* + 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.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 Object(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.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'] = res.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)); + } + } + + res.end(); + } + + // + // 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) { + return res.end(); + } + + // + // 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 { + res.write(chunk); + } + catch (er) { + try { res.end() } + catch (er) {} + } + } + }); + + // + // 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); + res.end(); + + // 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) { + reverseProxy.write(chunk); + } + }); + + // + // 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 && !errState) { + buffer.resume(); + } +}; + +// +// ### @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 Object(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) { + forwardProxy.write(chunk); + }) + + // + // 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. +// #### @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 Object(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(); + } + + // + // 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); + reverseProxy.incoming.socket.write(data); + } + catch (ex) { + 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); + proxySocket.write(data); + } + catch (ex) { + 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 = options.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 + // + 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.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); + socket.write(data); + } + 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. + // + reverseProxy.write(head); + } + catch (ex) { + return proxyError(ex); + } + + // + // If we have been passed buffered data, resume it. + // + if (buffer && !errState) { + buffer.resume(); + } +}; diff --git a/lib/proxy-table.js b/lib/node-http-proxy/proxy-table.js similarity index 98% rename from lib/proxy-table.js rename to lib/node-http-proxy/proxy-table.js index 90a129bc5..233e552f3 100644 --- a/lib/proxy-table.js +++ b/lib/node-http-proxy/proxy-table.js @@ -81,7 +81,9 @@ util.inherits(ProxyTable, events.EventEmitter); // 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; diff --git a/lib/node-http-proxy/routing-proxy.js b/lib/node-http-proxy/routing-proxy.js new file mode 100644 index 000000000..26887bdf3 --- /dev/null +++ b/lib/node-http-proxy/routing-proxy.js @@ -0,0 +1,4 @@ + +var RoutingProxy = exports.RoutingProxy = function () { + +}; \ No newline at end of file