diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 1e5df658bc004978c65af6c8fdf45efbef053011..aaf3a34bd817fbe6f78792dfba087016a87f6c22 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -1,28 +1,28 @@ /* - node-http-proxy.js: http proxy for node.js + node-http-proxy.js: http proxy for node.js - Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny + 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: + 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 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. + 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'), http = require('http'), @@ -49,25 +49,25 @@ var _agents = {}; // 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(':'); +function _getAgent(host, port, secure) { + var Agent, id = [host, port].join(':'); - if (!port) { - port = secure ? 443 : 80; - } + if (!port) { + port = secure ? 443 : 80; + } - if (!_agents[id]) { - Agent = secure ? https.Agent : http.Agent; + if (!_agents[id]) { + Agent = secure ? https.Agent : http.Agent; - _agents[id] = new Agent({ - host: host, - port: port - }); + _agents[id] = new Agent({ + host: host, + port: port + }); - _agents[id].maxSockets = maxSockets; - } + _agents[id].maxSockets = maxSockets; + } - return _agents[id]; + return _agents[id]; } // @@ -79,19 +79,19 @@ function _getAgent (host, port, secure) { // 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; +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]; - } - }) - } + if (typeof secure === 'object') { + outgoing = outgoing || {}; + ['ca', 'cert', 'key'].forEach(function (prop) { + if (secure[prop]) { + outgoing[prop] = secure[prop]; + } + }) + } - return protocol; + return protocol; } // @@ -101,7 +101,7 @@ function _getProtocol (secure, outgoing) { // made by __all__ instances of `HttpProxy` // exports.getMaxSockets = function () { - return maxSockets; + return maxSockets; }; // @@ -111,35 +111,35 @@ exports.getMaxSockets = function () { // made by __all__ instances of `HttpProxy` // exports.setMaxSockets = function (value) { - maxSockets = 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); - }; - }); +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; + return handle; } // @@ -155,104 +155,115 @@ exports.stack = function stack (middlewares, proxy) { // * `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; - }; - }); + 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); + 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 (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); + 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); + 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) { - 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 (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.') + } - 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 = options.https - ? https.createServer(options.https, handler) - : http.createServer(handler); + server.on('close', function () { + proxy.close(); + }); - server.on('close', function () { - proxy.close(); - }); + proxy.on('routes', function (routes) { + server.emit('routes', routes); + }); - 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 + }); + }); + } - 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; + // + // Set the proxy on the server so it is available + // to the consumer of the server + // + server.proxy = proxy; + return server; }; // @@ -275,34 +286,34 @@ exports.createServer = function () { // } // 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); - }); - } + 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 @@ -329,35 +340,35 @@ util.inherits(HttpProxy, events.EventEmitter); // [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]); - } - } - }; + 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]); + } + } + }; }; // @@ -366,9 +377,9 @@ HttpProxy.prototype.buffer = function (obj) { // if they exist. // HttpProxy.prototype.close = function () { - if (this.proxyTable) { - this.proxyTable.close(); - } + if (this.proxyTable) { + this.proxyTable.close(); + } }; // @@ -384,222 +395,230 @@ HttpProxy.prototype.close = function () { // 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; - } + var self = this, errState = false, location, outgoing, protocol, reverseProxy; // - // 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; - } + // 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); - 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)); - } - } + // + // 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); - try { - res.end(); - } catch (er) { - console.error("res.end error: %s", er.message); + // + // 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; } - } - - 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'; + + // + // 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'; } - // 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; + // + // 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); } - // 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) { + // + // #### 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.write(chunk); - } catch (er) { - console.error("res.write error: %s", er.message); - try { res.end(); - } catch (er) { + } catch (er) { console.error("res.end error: %s", er.message); - } } - } - }); + } - // 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); + 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) { + if (typeof options.postFlightMethod == 'function') { + options.postFlightMethod(response) } + handleResponse(response) + }) - // 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); + function handleResponse(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'; + } - // 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(); + + // 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 { + 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); + } + } + } + }); + + // 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); + } + }); } - }); - - // If we have been passed buffered data, resume it. - if (options.buffer) { - if (!errState) { - options.buffer.resume(); - } else { - options.buffer.destroy(); + + // 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) { + if (!errState) { + options.buffer.resume(); + } else { + options.buffer.destroy(); + } } - } }; // @@ -609,50 +628,51 @@ HttpProxy.prototype.proxyRequest = function (req, res, options) { // 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) { - forwardProxy.write(chunk); - }) - - // At the end of the client request, we are going to stop the proxied request - req.on('end', function () { - forwardProxy.end(); - }); + 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) { + forwardProxy.write(chunk); + }) + + // At the end of the client request, we are going to stop the proxied request + req.on('end', function () { + forwardProxy.end(); + }); }; // @@ -668,288 +688,290 @@ HttpProxy.prototype._forwardRequest = function (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; + 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; + 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 (this.proxyTable && !options.host) { + location = this.proxyTable.getProxyLocation(req); - if (!location) { - return socket.destroy(); - } + 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'); + options.port = location.port; + options.host = location.host; } - } - - // - // On `upgrade` from the Agent socket, listen to - // the appropriate events. - // - function onUpgrade (reverseProxy, proxySocket) { - if (!reverseProxy) { - proxySocket.end(); - socket.end(); - return; + + // + // 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; } // - // Any incoming data on this WebSocket to the proxy target - // will be written to the `reverseProxy` socket. + // Helper function for setting appropriate socket values: + // 1. Turn of all bufferings + // 2. For server set KeepAlive + // 3. For client set encoding // - 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); + 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); + } } - catch (e) { - detach(); - reverseProxy.incoming.socket.end(); - proxySocket.end(); + else { + socket.setEncoding('utf8'); } - } - }); + } // - // Any outgoing data on this Websocket from the proxy target - // will be written to the `proxySocket` socket. + // On `upgrade` from the Agent socket, listen to + // the appropriate events. // - reverseProxy.incoming.socket.on('data', listeners.onOutgoing = function(data) { - try { - self.emit('websocket:incoming', reverseProxy, reverseProxy.incoming, head, data); - proxySocket.write(data); - } - catch (e) { - detach(); - proxySocket.end(); - socket.end(); - } - }); + 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 (e) { + 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); + proxySocket.write(data); + } + catch (e) { + 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 + } // - // Helper function to detach all event listeners - // from `reverseProxy` and `proxySocket`. + // 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`. // - 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); + 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; } // - // If the incoming `proxySocket` socket closes, then - // detach all event listeners. + // Make the outgoing WebSocket request // - proxySocket.on('end', listeners.onIncomingClose = function() { - reverseProxy.incoming.socket.end(); - detach(); + outgoing = { + host: options.host, + port: options.port, + method: 'GET', + path: req.url, + headers: req.headers, + }; - // Emit the `end` event now that we have completed proxying - self.emit('websocket:end', req, socket, head); - }); + var reverseProxy = agent.appendMessage(outgoing); // - // If the `reverseProxy` socket closes, then detach all - // event listeners. + // On any errors from the `reverseProxy` emit the + // `webSocketProxyError` and close the appropriate + // connections. // - 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, - }; - - 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; + function proxyError(err) { + reverseProxy.end(); + if (self.emit('webSocketProxyError', req, socket, head)) { + return; + } + socket.end(); } - 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) { - proxyError(ex) - } - - // Catch socket errors - socket.on('error', proxyError); - - // Remove data listener now that the 'handshake' is complete - reverseProxy.socket.removeListener('data', handshake); - }); - } + // + // 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 + }; - reverseProxy.on('error', proxyError); + // + // 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); + }); + } - try { // - // Attempt to write the upgrade-head to the reverseProxy request. + // If the reverseProxy connection has an underlying socket, + // then execute the WebSocket handshake. // - reverseProxy.write(head); - } - catch (ex) { - proxyError(ex); - } + 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) { + 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) { - if (!errState) { - options.buffer.resume(); - } else { - options.buffer.destroy(); + // If we have been passed buffered data, resume it. + if (options.buffer) { + if (!errState) { + options.buffer.resume(); + } else { + options.buffer.destroy(); + } } - } };