master
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};