master
Raw Download raw file
  1// This file is a port of [node-http-proxy](https://github.com/nodejitsu/node-http-proxy)
  2// The purpose of the port is to enable connections to the backend
  3// server over a Unix socket file.
  4
  5// node-http-proxy includes the following message:
  6
  7// Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires
  8// Permission is hereby granted, free of charge, to any person obtaining
  9// a copy of this software and associated documentation files (the
 10// "Software"), to deal in the Software without restriction, including
 11// without limitation the rights to use, copy, modify, merge, publish,
 12// distribute, sublicense, and/or sell copies of the Software, and to
 13// permit persons to whom the Software is furnished to do so, subject to
 14// the following conditions:
 15
 16// The above copyright notice and this permission notice shall be
 17// included in all copies or substantial portions of the Software.
 18
 19// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 20// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 21// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 22// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 23// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 24// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 25// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 26
 27"use strict";
 28
 29var http = require('http');
 30var url = require('url')
 31var events = require('events');
 32var util = require('util');
 33
 34var HttpProxy = exports.HttpProxy = function(options) {
 35  events.EventEmitter.call(this);
 36  var self  = this;
 37  this.target = options.target;
 38  this.enable  = {};
 39  this.timeout = options.timeout;
 40}
 41
 42util.inherits(HttpProxy, events.EventEmitter);
 43
 44exports.createServer = function (options, callback) {
 45  var handlers = [callback];
 46  var proxy = new HttpProxy(options);
 47  function handler(req, res) {
 48    return callback(req, res, proxy);
 49  }
 50  var server = http.createServer(handler);
 51  server.on('close', function () {
 52    proxy.close();
 53  });
 54  server.proxy = proxy;
 55  return server;
 56}
 57
 58//
 59// ### function proxyRequest (req, res, buffer)
 60// #### @req {ServerRequest} Incoming HTTP Request to proxy.
 61// #### @res {ServerResponse} Outgoing HTTP Request to write proxied data to.
 62// #### @buffer {Object} Result from `httpProxy.buffer(req)`
 63//
 64HttpProxy.prototype.proxyRequest = function (req, res, buffer) {
 65  var self = this,
 66      errState = false,
 67      outgoing = {},
 68      reverseProxy,
 69      location;
 70
 71  // If this is a DELETE request then set the "content-length"
 72  // header (if it is not already set)
 73  if (req.method === 'DELETE') {
 74    req.headers['content-length'] = req.headers['content-length'] || '0';
 75  }
 76
 77  //
 78  // Add common proxy headers to the request so that they can
 79  // be availible to the proxy target server. If the proxy is
 80  // part of proxy chain it will append the address:
 81  //
 82  // * `x-forwarded-for`: IP Address of the original request
 83  // * `x-forwarded-proto`: Protocol of the original request
 84  // * `x-forwarded-port`: Port of the original request.
 85  //
 86  if (this.enable.xforward && req.connection && req.socket) {
 87    if (req.headers['x-forwarded-for']) {
 88      var addressToAppend = "," + req.connection.remoteAddress || req.socket.remoteAddress;
 89      req.headers['x-forwarded-for'] += addressToAppend;
 90    }
 91    else {
 92      req.headers['x-forwarded-for'] = req.connection.remoteAddress || req.socket.remoteAddress;
 93    }
 94
 95    if (req.headers['x-forwarded-port']) {
 96      var portToAppend = "," + getPortFromHostHeader(req);
 97      req.headers['x-forwarded-port'] += portToAppend;
 98    }
 99    else {
100      req.headers['x-forwarded-port'] = getPortFromHostHeader(req);
101    }
102
103    if (req.headers['x-forwarded-proto']) {
104      var protoToAppend = "," + getProto(req);
105      req.headers['x-forwarded-proto'] += protoToAppend;
106    }
107    else {
108      req.headers['x-forwarded-proto'] = getProto(req);
109    }
110  }
111
112  if (this.timeout) {
113    req.socket.setTimeout(this.timeout);
114  }
115
116  //
117  // Emit the `start` event indicating that we have begun the proxy operation.
118  //
119  this.emit('start', req, res, this.target);
120
121  //
122  // #### function proxyError (err)
123  // #### @err {Error} Error contacting the proxy target
124  // Short-circuits `res` in the event of any error when
125  // contacting the proxy target at `host` / `port`.
126  //
127  function proxyError(err) {
128    errState = true;
129
130    //
131    // Emit an `error` event, allowing the application to use custom
132    // error handling. The error handler should end the response.
133    //
134    if (self.emit('proxyError', err, req, res)) {
135      return;
136    }
137
138    res.writeHead(500, { 'Content-Type': 'application/json' });
139
140    if (req.method !== 'HEAD') {
141      var contents = {
142        'error': err
143      }
144      res.write(JSON.stringify(contents));
145    }
146
147    try { res.end() }
148    catch (ex) { console.error("res.end error: %s", ex.message) }
149  }
150
151  //
152  // Setup outgoing proxy with relevant properties.
153  //
154  outgoing.host       = this.target.host;
155  outgoing.hostname   = this.target.hostname;
156  outgoing.port       = this.target.port;
157  outgoing.socketPath = this.target.socketPath;
158  outgoing.agent      = this.target.agent;
159  outgoing.method     = req.method;
160  outgoing.path       = url.parse(req.url).path;
161  outgoing.headers    = req.headers;
162
163  //
164  // If the changeOrigin option is specified, change the
165  // origin of the host header to the target URL! Please
166  // don't revert this without documenting it!
167  //
168  if (this.changeOrigin) {
169    outgoing.headers.host = this.target.host;
170    // Only add port information to the header if not default port
171    // for this protocol.
172    // See https://github.com/nodejitsu/node-http-proxy/issues/458
173    if (this.target.port !== 443 && this.target.https ||
174      this.target.port !== 80 && !this.target.https) {
175      outgoing.headers.host += ':' + this.target.port;
176    }
177  }
178
179  //
180  // Open new HTTP request to internal resource with will act
181  // as a reverse proxy pass
182  //
183  reverseProxy = http.request(outgoing, function (response) {
184    //
185    // Process the `reverseProxy` `response` when it's received.
186    //
187    if (req.httpVersion === '1.0') {
188      if (req.headers.connection) {
189        response.headers.connection = req.headers.connection
190      } else {
191        response.headers.connection = 'close'
192      }
193    } else if (!response.headers.connection) {
194      if (req.headers.connection) { response.headers.connection = req.headers.connection }
195      else {
196        response.headers.connection = 'keep-alive'
197      }
198    }
199
200    // Remove `Transfer-Encoding` header if client's protocol is HTTP/1.0
201    // or if this is a DELETE request with no content-length header.
202    // See: https://github.com/nodejitsu/node-http-proxy/pull/373
203    if (req.httpVersion === '1.0' || (req.method === 'DELETE'
204      && !req.headers['content-length'])) {
205      delete response.headers['transfer-encoding'];
206    }
207
208    if ((response.statusCode === 301 || response.statusCode === 302)
209      && typeof response.headers.location !== 'undefined') {
210      location = url.parse(response.headers.location);
211      if (location.host === req.headers.host) {
212        if (self.source.https && !self.target.https) {
213          response.headers.location = response.headers.location.replace(/^http\:/, 'https:');
214        }
215        if (self.target.https && !self.source.https) {
216          response.headers.location = response.headers.location.replace(/^https\:/, 'http:');
217        }
218      }
219    }
220
221    //
222    // When the `reverseProxy` `response` ends, end the
223    // corresponding outgoing `res` unless we have entered
224    // an error state. In which case, assume `res.end()` has
225    // already been called and the 'error' event listener
226    // removed.
227    //
228    var ended = false;
229    response.on('close', function () {
230      if (!ended) { response.emit('end') }
231    });
232
233    //
234    // After reading a chunked response, the underlying socket
235    // will hit EOF and emit a 'end' event, which will abort
236    // the request. If the socket was paused at that time,
237    // pending data gets discarded, truncating the response.
238    // This code makes sure that we flush pending data.
239    //
240    response.connection.on('end', function () {
241      if (response.readable && response.resume) {
242        response.resume();
243      }
244    });
245
246    response.on('end', function () {
247      ended = true;
248      if (!errState) {
249        try { res.end() }
250        catch (ex) { console.error("res.end error: %s", ex.message) }
251
252        // Emit the `end` event now that we have completed proxying
253        self.emit('end', req, res, response);
254      }
255    });
256
257    // Allow observer to modify headers or abort response
258    try { self.emit('proxyResponse', req, res, response) }
259    catch (ex) {
260      errState = true;
261      return;
262    }
263
264    // Set the headers of the client response
265    if (res.sentHeaders !== true) {
266      Object.keys(response.headers).forEach(function (key) {
267        res.setHeader(key, response.headers[key]);
268      });
269      res.writeHead(response.statusCode);
270    }
271
272    function ondata(chunk) {
273      if (res.writable) {
274        // Only pause if the underlying buffers are full,
275        // *and* the connection is not in 'closing' state.
276        // Otherwise, the pause will cause pending data to
277        // be discarded and silently lost.
278        if (false === res.write(chunk) && response.pause
279            && response.connection.readable) {
280          response.pause();
281        }
282      }
283    }
284
285    response.on('data', ondata);
286
287    function ondrain() {
288      if (response.readable && response.resume) {
289        response.resume();
290      }
291    }
292
293    res.on('drain', ondrain);
294  });
295
296  // allow unlimited listeners
297  reverseProxy.setMaxListeners(0);
298
299  //
300  // Handle 'error' events from the `reverseProxy`. Setup timeout override if needed
301  //
302  reverseProxy.once('error', proxyError);
303
304  // Set a timeout on the socket if `this.timeout` is specified.
305  reverseProxy.once('socket', function (socket) {
306    if (self.timeout) {
307      socket.setTimeout(self.timeout);
308    }
309  });
310
311  //
312  // Handle 'error' events from the `req` (e.g. `Parse Error`).
313  //
314  req.on('error', proxyError);
315
316  //
317  // If `req` is aborted, we abort our `reverseProxy` request as well.
318  //
319  req.on('aborted', function () {
320    reverseProxy.abort();
321  });
322
323  //
324  // For each data `chunk` received from the incoming
325  // `req` write it to the `reverseProxy` request.
326  //
327  req.on('data', function (chunk) {
328    if (!errState) {
329      var flushed = reverseProxy.write(chunk);
330      if (!flushed) {
331        req.pause();
332        reverseProxy.once('drain', function () {
333          try { req.resume() }
334          catch (er) { console.error("req.resume error: %s", er.message) }
335        });
336
337        //
338        // Force the `drain` event in 100ms if it hasn't
339        // happened on its own.
340        //
341        setTimeout(function () {
342          reverseProxy.emit('drain');
343        }, 100);
344      }
345    }
346  });
347
348  //
349  // When the incoming `req` ends, end the corresponding `reverseProxy`
350  // request unless we have entered an error state.
351  //
352  req.on('end', function () {
353    if (!errState) {
354      reverseProxy.end();
355    }
356  });
357
358  // Aborts reverseProxy if client aborts the connection.
359  req.on('close', function () {
360    if (!errState) {
361      reverseProxy.abort();
362    }
363  });
364
365  //
366  // If we have been passed buffered data, resume it.
367  //
368  if (buffer) {
369    return !errState
370      ? buffer.resume()
371      : buffer.destroy();
372  }
373};
374
375exports.buffer = function (obj) {
376  var events = [],
377      onData,
378      onEnd;
379
380  obj.on('data', onData = function (data, encoding) {
381    events.push(['data', data, encoding]);
382  });
383
384  obj.on('end', onEnd = function (data, encoding) {
385    events.push(['end', data, encoding]);
386  });
387
388  return {
389    end: function () {
390      obj.removeListener('data', onData);
391      obj.removeListener('end', onEnd);
392    },
393    destroy: function () {
394      this.end();
395        this.resume = function () {
396          console.error("Cannot resume buffer after destroying it.");
397        };
398
399        onData = onEnd = events = obj = null;
400    },
401    resume: function () {
402      this.end();
403      for (var i = 0, len = events.length; i < len; ++i) {
404        obj.emit.apply(obj, events[i]);
405      }
406    }
407  };
408};