diff --git a/.gitignore b/.gitignore index 468b525c9..967876b80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ config.json node_modules/ npm-debug.log +*.swp diff --git a/lib/node-http-proxy.js b/lib/node-http-proxy.js index 956a5f3d3..922c6d8f0 100644 --- a/lib/node-http-proxy.js +++ b/lib/node-http-proxy.js @@ -24,10 +24,10 @@ */ -var util = require('util'), - http = require('http'), - https = require('https'), - events = require('events'), +var util = require('util'), + http = require('http'), + https = require('https'), + events = require('events'), maxSockets = 100; // @@ -392,3 +392,23 @@ exports._getBase = function _getBase (options) { return result; }; + +exports._setupOutgoing = function(outgoing, options, req) { + [ + 'host', + 'hostname', + 'port', + 'socketPath', + 'agent' + ].forEach(function(elem) { + outgoing[elem] = options[elem]; + }); + + [ + 'method', + 'path', + 'headers' + ].forEach(function(elem) { + outgoing[elem] = req[elem]; + }); +}; \ No newline at end of file diff --git a/lib/node-http-proxy/http-proxy.js b/lib/node-http-proxy/http-proxy.js index 0efb2fa3c..379e9dddb 100644 --- a/lib/node-http-proxy/http-proxy.js +++ b/lib/node-http-proxy/http-proxy.js @@ -24,11 +24,13 @@ */ -var events = require('events'), - http = require('http'), - util = require('util'), - url = require('url'), - httpProxy = require('../node-http-proxy'); +var events = require('events'), + http = require('http'), + util = require('util'), + url = require('url'), + ForwardStream = require('./streams/forward'), + ProxyStream = require('./streams/proxy'), + httpProxy = require('../node-http-proxy'); // // ### function HttpProxy (options) @@ -66,7 +68,7 @@ var HttpProxy = exports.HttpProxy = function (options) { // this.forward = options.forward; this.target = options.target; - this.timeout = options.timeout; + this.timeout = options.timeout; // // Setup the necessary instances instance variables for @@ -91,10 +93,11 @@ var HttpProxy = exports.HttpProxy = function (options) { // // Setup opt-in features // - this.enable = options.enable || {}; - this.enable.xforward = typeof this.enable.xforward === 'boolean' - ? this.enable.xforward - : true; + this.enable = options.enable || {}; + + if(typeof this.enable.xforward !== 'boolean') { + this.enable.xforward = true; + } // // Setup additional options for WebSocket proxying. When forcing @@ -126,8 +129,8 @@ HttpProxy.prototype.proxyRequest = function (req, res, buffer) { // If this is a DELETE request then set the "content-length" // header (if it is not already set) - if (req.method === 'DELETE') { - req.headers['content-length'] = req.headers['content-length'] || '0'; + if (req.method === 'DELETE' && !req.headers['content-length']) { + req.headers['content-length'] = '0'; } // @@ -140,29 +143,17 @@ HttpProxy.prototype.proxyRequest = function (req, res, buffer) { // * `x-forwarded-port`: Port of the original request. // if (this.enable.xforward && req.connection && req.socket) { - if (req.headers['x-forwarded-for']) { - var addressToAppend = "," + req.connection.remoteAddress || req.socket.remoteAddress; - req.headers['x-forwarded-for'] += addressToAppend; - } - else { - req.headers['x-forwarded-for'] = req.connection.remoteAddress || req.socket.remoteAddress; - } - - if (req.headers['x-forwarded-port']) { - var portToAppend = "," + req.connection.remotePort || req.socket.remotePort; - req.headers['x-forwarded-port'] += portToAppend; - } - else { - req.headers['x-forwarded-port'] = req.connection.remotePort || req.socket.remotePort; - } - - if (req.headers['x-forwarded-proto']) { - var protoToAppend = "," + getProto(req); - req.headers['x-forwarded-proto'] += protoToAppend; - } - else { - req.headers['x-forwarded-proto'] = getProto(req); - } + req.headers['x-forwarded-for'] = (req.headers['x-forwarded-for'] || '') + + (req.headers['x-forwarded-for'] ? ',' : '') + + (req.connection.remoteAddress || socket.remoteAddress); + + req.headers['x-forwarded-port'] = (req.headers['x-forwarded-port'] || '') + + (req.headers['x-forwarded-port'] ? ',' : '') + + (req.connection.remotePort || socket.remotePort); + + req.headers['x-forwarded-proto'] = (req.headers['x-forwarded-proto'] || '') + + (req.headers['x-forwarded-proto'] ? ',' : '') + + getProto(req); } if (this.timeout) { @@ -173,6 +164,7 @@ HttpProxy.prototype.proxyRequest = function (req, res, buffer) { // Emit the `start` event indicating that we have begun the proxy operation. // this.emit('start', req, res, this.target); + req.pipe(new ProxyStream(res, this)).pipe(res); // // If forwarding is enabled for this instance, foward proxy the @@ -180,254 +172,7 @@ HttpProxy.prototype.proxyRequest = function (req, res, buffer) { // if (this.forward) { this.emit('forward', req, res, this.forward); - this._forwardRequest(req); - } - - // - // #### function proxyError (err) - // #### @err {Error} Error contacting the proxy target - // Short-circuits `res` in the event of any error when - // contacting the proxy target at `host` / `port`. - // - function proxyError(err) { - errState = true; - - // - // Emit an `error` event, allowing the application to use custom - // error handling. The error handler should end the response. - // - if (self.emit('proxyError', err, req, res)) { - return; - } - - res.writeHead(500, { 'Content-Type': 'text/plain' }); - - if (req.method !== 'HEAD') { - // - // This NODE_ENV=production behavior is mimics Express and - // Connect. - // - if (process.env.NODE_ENV === 'production') { - res.write('Internal Server Error'); - } - else { - res.write('An error has occurred: ' + JSON.stringify(err)); - } - } - - try { res.end() } - catch (ex) { console.error("res.end error: %s", ex.message) } - } - - // - // Setup outgoing proxy with relevant properties. - // - outgoing.host = this.target.host; - outgoing.hostname = this.target.hostname; - outgoing.port = this.target.port; - outgoing.socketPath = this.target.socketPath; - outgoing.agent = this.target.agent; - outgoing.method = req.method; - outgoing.path = req.url; - outgoing.headers = req.headers; - - // - // If the changeOrigin option is specified, change the - // origin of the host header to the target URL! Please - // don't revert this without documenting it! - // - if (this.changeOrigin) { - outgoing.headers.host = this.target.host + ':' + this.target.port; - } - - // - // 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 (req.httpVersion === '1.0') { - if (req.headers.connection) { - response.headers.connection = req.headers.connection - } else { - response.headers.connection = 'close' - } - } else if (!response.headers.connection) { - if (req.headers.connection) { response.headers.connection = req.headers.connection } - else { - response.headers.connection = 'keep-alive' - } - } - - // Remove `Transfer-Encoding` header if client's protocol is HTTP/1.0 - // or if this is a DELETE request with no content-length header. - // See: https://github.com/nodejitsu/node-http-proxy/pull/373 - if (req.httpVersion === '1.0' || (req.method === 'DELETE' - && !req.headers['content-length'])) { - delete response.headers['transfer-encoding']; - } - - if ((response.statusCode === 301 || response.statusCode === 302) - && typeof response.headers.location !== 'undefined') { - location = url.parse(response.headers.location); - if (location.host === req.headers.host) { - if (self.source.https && !self.target.https) { - response.headers.location = response.headers.location.replace(/^http\:/, 'https:'); - } - if (self.target.https && !self.source.https) { - response.headers.location = response.headers.location.replace(/^https\:/, 'http:'); - } - } - } - - // - // 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. - // - var ended = false; - response.on('close', function () { - if (!ended) { response.emit('end') } - }); - - // - // After reading a chunked response, the underlying socket - // will hit EOF and emit a 'end' event, which will abort - // the request. If the socket was paused at that time, - // pending data gets discarded, truncating the response. - // This code makes sure that we flush pending data. - // - response.connection.on('end', function () { - if (response.readable && response.resume) { - response.resume(); - } - }); - - response.on('end', function () { - ended = true; - if (!errState) { - try { res.end() } - catch (ex) { console.error("res.end error: %s", ex.message) } - - // Emit the `end` event now that we have completed proxying - self.emit('end', req, res, response); - } - }); - - // Allow observer to modify headers or abort response - try { self.emit('proxyResponse', req, res, response) } - catch (ex) { - errState = true; - return; - } - - // Set the headers of the client response - Object.keys(response.headers).forEach(function (key) { - res.setHeader(key, response.headers[key]); - }); - res.writeHead(response.statusCode); - - function ondata(chunk) { - if (res.writable) { - // Only pause if the underlying buffers are full, - // *and* the connection is not in 'closing' state. - // Otherwise, the pause will cause pending data to - // be discarded and silently lost. - if (false === res.write(chunk) && response.pause - && response.connection.readable) { - response.pause(); - } - } - } - - response.on('data', ondata); - - function ondrain() { - if (response.readable && response.resume) { - response.resume(); - } - } - - res.on('drain', ondrain); - }); - - // - // Handle 'error' events from the `reverseProxy`. Setup timeout override if needed - // - reverseProxy.once('error', proxyError); - - // Set a timeout on the socket if `this.timeout` is specified. - reverseProxy.once('socket', function (socket) { - if (self.timeout) { - socket.setTimeout(self.timeout); - } - }); - - // - // Handle 'error' events from the `req` (e.g. `Parse Error`). - // - req.on('error', proxyError); - - // - // If `req` is aborted, we abort our `reverseProxy` request as well. - // - req.on('aborted', function () { - reverseProxy.abort(); - }); - - // - // For each data `chunk` received from the incoming - // `req` write it to the `reverseProxy` request. - // - req.on('data', function (chunk) { - if (!errState) { - var flushed = reverseProxy.write(chunk); - if (!flushed) { - req.pause(); - reverseProxy.once('drain', function () { - try { req.resume() } - catch (er) { console.error("req.resume error: %s", er.message) } - }); - - // - // Force the `drain` event in 100ms if it hasn't - // happened on its own. - // - setTimeout(function () { - reverseProxy.emit('drain'); - }, 100); - } - } - }); - - // - // When the incoming `req` ends, end the corresponding `reverseProxy` - // request unless we have entered an error state. - // - req.on('end', function () { - if (!errState) { - reverseProxy.end(); - } - }); - - //Aborts reverseProxy if client aborts the connection. - req.on('close', function () { - if (!errState) { - reverseProxy.abort(); - } - }); - - // - // If we have been passed buffered data, resume it. - // - if (buffer) { - return !errState - ? buffer.resume() - : buffer.destroy(); + req.pipe(new ForwardStream(this.forward)); } }; @@ -471,29 +216,17 @@ HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, upgradeHead, // * `x-forwarded-port`: Port of the original request. // if (this.enable.xforward && req.connection) { - if (req.headers['x-forwarded-for']) { - var addressToAppend = "," + req.connection.remoteAddress || socket.remoteAddress; - req.headers['x-forwarded-for'] += addressToAppend; - } - else { - req.headers['x-forwarded-for'] = req.connection.remoteAddress || socket.remoteAddress; - } - - if (req.headers['x-forwarded-port']) { - var portToAppend = "," + req.connection.remotePort || socket.remotePort; - req.headers['x-forwarded-port'] += portToAppend; - } - else { - req.headers['x-forwarded-port'] = req.connection.remotePort || socket.remotePort; - } - - if (req.headers['x-forwarded-proto']) { - var protoToAppend = "," + (req.connection.pair ? 'wss' : 'ws'); - req.headers['x-forwarded-proto'] += protoToAppend; - } - else { - req.headers['x-forwarded-proto'] = req.connection.pair ? 'wss' : 'ws'; - } + req.headers['x-forwarded-for'] = (req.headers['x-forwarded-for'] || '') + + (req.headers['x-forwarded-for'] ? ',' : '') + + (req.connection.remoteAddress || socket.remoteAddress); + + req.headers['x-forwarded-port'] = (req.headers['x-forwarded-port'] || '') + + (req.headers['x-forwarded-port'] ? ',' : '') + + (req.connection.remotePort || socket.remotePort); + + req.headers['x-forwarded-proto'] = (req.headers['x-forwarded-proto'] || '') + + (req.headers['x-forwarded-proto'] ? ',' : '') + + (req.connection.pair ? 'wss' : 'ws'); } self.emit('websocket:start', req, socket, head, this.target); diff --git a/lib/node-http-proxy/streams/forward.js b/lib/node-http-proxy/streams/forward.js new file mode 100644 index 000000000..c771a8902 --- /dev/null +++ b/lib/node-http-proxy/streams/forward.js @@ -0,0 +1,34 @@ +var Writable = require('stream').Writable, + proxy = require('../../node-http-proxy'); + http = require('http'), + https = require('https'), + util = require('util'); + +var ForwardStream = module.exports = function ForwardStream(options) { + Writable.call(this); + + var self = this; + + this.once('pipe', function(req) { + var protocol = options.https ? https : http, + outgoing = proxy._getBase(options); + + proxy._setupOutgoing(outgoing, options, req); + + // pipe throw-safe? do we need to add a ` on 'error' ` handler? + self.request = protocol.request(outgoing, function() {}); + self.request.on('error', function() { }); + + self.on('finish', function() { + self.request.end(); + }); + }); + +}; + +ForwardStream.prototype._write = function(chunk, encoding, callback) { + this.request.write(chunk, encoding, callback); +}; + +util.inherits(ForwardStream, Writable); + diff --git a/lib/node-http-proxy/streams/proxy.js b/lib/node-http-proxy/streams/proxy.js new file mode 100644 index 000000000..63b6a9c1d --- /dev/null +++ b/lib/node-http-proxy/streams/proxy.js @@ -0,0 +1,136 @@ +var Duplex = require('stream').Duplex, + proxy = require('../../node-http-proxy'); + http = require('http'), + https = require('https'), + url = require('url'), + util = require('util'); + +var ProxyStream = module.exports = function ProxyStream(response, options) { + Duplex.call(this); + + var self = this, + target = options.target, + source = options.source; + + this.once('pipe', function(req) { + var protocol = target.https ? https : http, + outgoing = proxy._getBase(target); + + proxy._setupOutgoing(outgoing, target, req); + + if (options.changeOrigin) { + outgoing.headers.host = target.host + ':' + target.port; + } + + self.request = protocol.request(outgoing); + self.on('finish', function() { + self.request.end(); + }); + + self.request.on('response', function (res) { + self.response = res; + if(req.httpVersion === '1.0') { + res.headers.connection = req.headers.connection || 'close'; + } + else if(!res.headers.connection) { + res.headers.connection = req.headers.connection || 'keep-alive'; + } + + if(req.httpVersion === '1.0' || (req.method === 'DELETE' && !req.headers['content-length'])) { + delete res.headers['transfer-encoding']; + } + + if(~[301,302].indexOf(res.statusCode) && typeof res.headers.location !== 'undefined') { + var location = url.parse(res.headers.location); + if ( + location.host === req.headers.host && + ( + source.https && !target.https || + target.https && !source.https + ) + ) { + res.headers.location = res.headers.location.replace(/^https\:/, 'http:'); + } + } + + self.emit('proxyResponse', req, response, res); + + Object.keys(res.headers).forEach(function (key) { + response.setHeader(key, res.headers[key]); + }); + response.writeHead(response.statusCode); + + res.on('readable', function() { + self.read(0); + }); + + res.on('end', function() { + self.push(null); + }); + self.emit('readable'); + }); + + // HACK: please fix it + // Should we call end() splicity? + self.end(); + + + // + // Handle 'error' events from the `reverseProxy`. Setup timeout override if needed + // + self.request.once('error', proxyError); +/* + // Set a timeout on the socket if `this.timeout` is specified. + reverseProxy.once('socket', function (socket) { + if (self.timeout) { + socket.setTimeout(self.timeout); + } + }); */ + + // + // #### 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) { + // + // 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; + // } + + response.writeHead(500, { 'Content-Type': 'text/plain' }); + if (self.request.method !== 'HEAD') { + // + // This NODE_ENV=production behavior is mimics Express and + // Connect. + // + response.write(process.env.NODE_ENV === 'production' + ? 'Internal Server Error' + : 'An error has occurred: ' + JSON.stringify(err) + ); + } + + try { response.end() } + catch (ex) { console.error("res.end error: %s", ex.message) } + } + }); + +}; + +ProxyStream.prototype = Object.create( + Duplex.prototype, { constructor: { value: ProxyStream } } +); + +ProxyStream.prototype._write = function(chunk, encoding, callback) { + this.request.write(chunk, encoding, callback); +}; + +ProxyStream.prototype._read = function(size) { + var chunk = this.response ? this.response.read(size) : ''; + if(chunk == null) { chunk = '' } + this.push(chunk); +};