node-http-proxy.js (19721B)
1 /* 2 node-http-proxy.js: http proxy for node.js 3 4 Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny 5 6 Permission is hereby granted, free of charge, to any person obtaining 7 a copy of this software and associated documentation files (the 8 "Software"), to deal in the Software without restriction, including 9 without limitation the rights to use, copy, modify, merge, publish, 10 distribute, sublicense, and/or sell copies of the Software, and to 11 permit persons to whom the Software is furnished to do so, subject to 12 the following conditions: 13 14 The above copyright notice and this permission notice shall be 15 included in all copies or substantial portions of the Software. 16 17 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 25 */ 26 27 var util = require('util'), 28 http = require('http'), 29 https = require('https'), 30 events = require('events'), 31 ProxyTable = require('./proxy-table').ProxyTable, 32 maxSockets = 100; 33 34 // 35 // ### Version 0.5.0 36 // 37 exports.version = [0, 5, 0]; 38 39 // 40 // ### function _getAgent (host, port, secure) 41 // #### @host {string} Host of the agent to get 42 // #### @port {number} Port of the agent to get 43 // #### @secure {boolean} Value indicating whether or not to use HTTPS 44 // Retreives an agent from the `http` or `https` module 45 // and sets the `maxSockets` property appropriately. 46 // 47 function _getAgent (host, port, secure) { 48 var agent = !secure ? http.getAgent(host, port) : https.getAgent({ 49 host: host, 50 port: port 51 }); 52 53 agent.maxSockets = maxSockets; 54 return agent; 55 } 56 57 // 58 // ### function _getProtocol (secure, outgoing) 59 // #### @secure {Object|boolean} Settings for `https` 60 // #### @outgoing {Object} Outgoing request options 61 // Returns the appropriate protocol based on the settings in 62 // `secure`. If the protocol is `https` this function will update 63 // the options in `outgoing` as appropriate by adding `ca`, `key`, 64 // and `cert` if they exist in `secure`. 65 // 66 function _getProtocol (secure, outgoing) { 67 var protocol = secure ? https : http; 68 69 if (typeof secure === 'object') { 70 outgoing = outgoing || {}; 71 ['ca', 'cert', 'key'].forEach(function (prop) { 72 if (secure[prop]) { 73 outgoing[prop] = secure[prop]; 74 } 75 }) 76 } 77 78 return protocol; 79 } 80 81 // 82 // ### function getMaxSockets () 83 // Returns the maximum number of sockets 84 // allowed on __every__ outgoing request 85 // made by __all__ instances of `HttpProxy` 86 // 87 exports.getMaxSockets = function () { 88 return maxSockets; 89 }; 90 91 // 92 // ### function setMaxSockets () 93 // Sets the maximum number of sockets 94 // allowed on __every__ outgoing request 95 // made by __all__ instances of `HttpProxy` 96 // 97 exports.setMaxSockets = function (value) { 98 maxSockets = value; 99 }; 100 101 // 102 // ### function createServer ([port, host, options, handler]) 103 // #### @port {number} **Optional** Port to use on the proxy target host. 104 // #### @host {string} **Optional** Host of the proxy target. 105 // #### @options {Object} **Optional** Options for the HttpProxy instance used 106 // #### @handler {function} **Optional** Request handler for the server 107 // Returns a server that manages an instance of HttpProxy. Flexible arguments allow for: 108 // 109 // * `httpProxy.createServer(9000, 'localhost')` 110 // * `httpProxy.createServer(9000, 'localhost', options) 111 // * `httpPRoxy.createServer(function (req, res, proxy) { ... })` 112 // 113 exports.createServer = function () { 114 var args = Array.prototype.slice.call(arguments), 115 callback = typeof args[0] === 'function' && args.shift(), 116 options = {}, 117 port, host, forward, silent, proxy, server; 118 119 if (args.length >= 2) { 120 port = args[0]; 121 host = args[1]; 122 options = args[2] || {}; 123 } 124 else if (args.length === 1) { 125 options = args[0] || {}; 126 if (!options.router && !callback) { 127 throw new Error('Cannot create server with no router and no callback'); 128 } 129 } 130 131 proxy = new HttpProxy(options); 132 133 handler = function (req, res) { 134 if (callback) { 135 // 136 // If we were passed a callback to process the request 137 // or response in some way, then call it. 138 // 139 callback(req, res, proxy); 140 } 141 else if (port && host) { 142 // 143 // If we have a target host and port for the request 144 // then proxy to the specified location. 145 // 146 proxy.proxyRequest(req, res, { 147 port: port, 148 host: host 149 }); 150 } 151 else if (proxy.proxyTable) { 152 // 153 // If the proxy is configured with a ProxyTable 154 // instance then use that before failing. 155 // 156 proxy.proxyRequest(req, res); 157 } 158 else { 159 // 160 // Otherwise this server is improperly configured. 161 // 162 throw new Error('Cannot proxy without port, host, or router.') 163 } 164 }; 165 166 server = options.https 167 ? https.createServer(options.https, handler) 168 : http.createServer(handler); 169 170 server.on('close', function () { 171 proxy.close(); 172 }); 173 174 proxy.on('routes', function (routes) { 175 server.emit('routes', routes); 176 }); 177 178 if (!callback) { 179 // WebSocket support: if callback is empty tunnel 180 // websocket request automatically 181 server.on('upgrade', function(req, socket, head) { 182 // Tunnel websocket requests too 183 184 proxy.proxyWebSocketRequest(req, socket, head, { 185 port: port, 186 host: host 187 }); 188 }); 189 } 190 191 // 192 // Set the proxy on the server so it is available 193 // to the consumer of the server 194 // 195 server.proxy = proxy; 196 197 return server; 198 }; 199 200 // 201 // ### function HttpProxy (options) 202 // #### @options {Object} Options for this instance. 203 // Constructor function for new instances of HttpProxy responsible 204 // for managing the life-cycle of streaming reverse proxyied HTTP requests. 205 // 206 // Example options: 207 // 208 // { 209 // router: { 210 // 'foo.com': 'localhost:8080', 211 // 'bar.com': 'localhost:8081' 212 // }, 213 // forward: { 214 // host: 'localhost', 215 // port: 9001 216 // } 217 // } 218 // 219 var HttpProxy = exports.HttpProxy = function (options) { 220 events.EventEmitter.call(this); 221 222 var self = this; 223 options = options || {}; 224 this.forward = options.forward; 225 this.https = options.https; 226 227 if (options.router) { 228 this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly); 229 this.proxyTable.on('routes', function (routes) { 230 self.emit('routes', routes); 231 }); 232 } 233 }; 234 235 // Inherit from events.EventEmitter 236 util.inherits(HttpProxy, events.EventEmitter); 237 238 // 239 // ### function buffer (obj) 240 // #### @obj {Object} Object to pause events from 241 // Buffer `data` and `end` events from the given `obj`. 242 // Consumers of HttpProxy performing async tasks 243 // __must__ utilize this utility, to re-emit data once 244 // the async operation has completed, otherwise these 245 // __events will be lost.__ 246 // 247 // var buffer = httpProxy.buffer(req); 248 // fs.readFile(path, function(){ 249 // httpProxy.proxyRequest(req, res, host, port, buffer); 250 // }); 251 // 252 // __Attribution:__ This approach is based heavily on 253 // [Connect](https://github.com/senchalabs/connect/blob/master/lib/utils.js#L157). 254 // However, this is not a big leap from the implementation in node-http-proxy < 0.4.0. 255 // This simply chooses to manage the scope of the events on a new Object literal as opposed to 256 // [on the HttpProxy instance](https://github.com/nodejitsu/node-http-proxy/blob/v0.3.1/lib/node-http-proxy.js#L154). 257 // 258 HttpProxy.prototype.buffer = function (obj) { 259 var onData, onEnd, events = []; 260 261 obj.on('data', onData = function (data, encoding) { 262 events.push(['data', data, encoding]); 263 }); 264 265 obj.on('end', onEnd = function (data, encoding) { 266 events.push(['end', data, encoding]); 267 }); 268 269 return { 270 end: function () { 271 obj.removeListener('data', onData); 272 obj.removeListener('end', onEnd); 273 }, 274 resume: function () { 275 this.end(); 276 for (var i = 0, len = events.length; i < len; ++i) { 277 obj.emit.apply(obj, events[i]); 278 } 279 } 280 }; 281 }; 282 283 // 284 // ### function close () 285 // Frees the resources associated with this instance, 286 // if they exist. 287 // 288 HttpProxy.prototype.close = function () { 289 if (this.proxyTable) this.proxyTable.close(); 290 }; 291 292 // 293 // ### function proxyRequest (req, res, [port, host, paused]) 294 // #### @req {ServerRequest} Incoming HTTP Request to proxy. 295 // #### @res {ServerResponse} Outgoing HTTP Request to write proxied data to. 296 // #### @options {Object} Options for the outgoing proxy request. 297 // options.port {number} Port to use on the proxy target host. 298 // options.host {string} Host of the proxy target. 299 // options.buffer {Object} Result from `httpProxy.buffer(req)` 300 // options.https {Object|boolean} Settings for https. 301 // 302 HttpProxy.prototype.proxyRequest = function (req, res, options) { 303 var self = this, errState = false, location, outgoing, protocol, reverseProxy; 304 305 // Create an empty options hash if none is passed. 306 options = options || {}; 307 308 // 309 // Check the proxy table for this instance to see if we need 310 // to get the proxy location for the request supplied. We will 311 // always ignore the proxyTable if an explicit `port` and `host` 312 // arguments are supplied to `proxyRequest`. 313 // 314 if (this.proxyTable && !options.host) { 315 location = this.proxyTable.getProxyLocation(req); 316 317 // 318 // If no location is returned from the ProxyTable instance 319 // then respond with `404` since we do not have a valid proxy target. 320 // 321 if (!location) { 322 res.writeHead(404); 323 return res.end(); 324 } 325 326 // 327 // When using the ProxyTable in conjunction with an HttpProxy instance 328 // only the following arguments are valid: 329 // 330 // * `proxy.proxyRequest(req, res, { host: 'localhost' })`: This will be skipped 331 // * `proxy.proxyRequest(req, res, { buffer: buffer })`: Buffer will get updated appropriately 332 // * `proxy.proxyRequest(req, res)`: Options will be assigned appropriately. 333 // 334 options.port = location.port; 335 options.host = location.host; 336 } 337 338 // 339 // Add `x-forwarded-for` header to availible client IP to apps behind proxy 340 // 341 req.headers['x-forwarded-for'] = req.connection.remoteAddress; 342 343 // 344 // Emit the `start` event indicating that we have begun the proxy operation. 345 // 346 this.emit('start', req, res, options); 347 348 // 349 // If forwarding is enabled for this instance, foward proxy the 350 // specified request to the address provided in `this.forward` 351 // 352 if (this.forward) { 353 this.emit('forward', req, res, this.forward); 354 this._forwardRequest(req); 355 } 356 357 // 358 // #### function proxyError (err) 359 // #### @err {Error} Error contacting the proxy target 360 // Short-circuits `res` in the event of any error when 361 // contacting the proxy target at `host` / `port`. 362 // 363 function proxyError(err) { 364 errState = true; 365 res.writeHead(500, { 'Content-Type': 'text/plain' }); 366 367 if (req.method !== 'HEAD') { 368 res.write('An error has occurred: ' + JSON.stringify(err)); 369 } 370 371 res.end(); 372 } 373 374 outgoing = { 375 host: options.host, 376 port: options.port, 377 agent: _getAgent(options.host, options.port, options.https || this.https), 378 method: req.method, 379 path: req.url, 380 headers: req.headers 381 }; 382 383 // Force the `connection` header to be 'close' until 384 // node.js core re-implements 'keep-alive'. 385 outgoing.headers['connection'] = 'close'; 386 387 protocol = _getProtocol(options.https || this.https, outgoing); 388 389 // Open new HTTP request to internal resource with will act as a reverse proxy pass 390 reverseProxy = protocol.request(outgoing, function (response) { 391 392 // Process the `reverseProxy` `response` when it's received. 393 if (response.headers.connection) { 394 if (req.headers.connection) response.headers.connection = req.headers.connection; 395 else response.headers.connection = 'close'; 396 } 397 398 // Set the headers of the client response 399 res.writeHead(response.statusCode, response.headers); 400 401 // `response.statusCode === 304`: No 'data' event and no 'end' 402 if (response.statusCode === 304) { 403 return res.end(); 404 } 405 406 // For each data `chunk` received from the `reverseProxy` 407 // `response` write it to the outgoing `res`. 408 response.on('data', function (chunk) { 409 if (req.method !== 'HEAD') { 410 res.write(chunk); 411 } 412 }); 413 414 // When the `reverseProxy` `response` ends, end the 415 // corresponding outgoing `res` unless we have entered 416 // an error state. In which case, assume `res.end()` has 417 // already been called and the 'error' event listener 418 // removed. 419 response.on('end', function () { 420 if (!errState) { 421 reverseProxy.removeListener('error', proxyError); 422 res.end(); 423 424 // Emit the `end` event now that we have completed proxying 425 self.emit('end', req, res); 426 } 427 }); 428 }); 429 430 // Handle 'error' events from the `reverseProxy`. 431 reverseProxy.once('error', proxyError); 432 433 // For each data `chunk` received from the incoming 434 // `req` write it to the `reverseProxy` request. 435 req.on('data', function (chunk) { 436 if (!errState) { 437 reverseProxy.write(chunk); 438 } 439 }); 440 441 // 442 // When the incoming `req` ends, end the corresponding `reverseProxy` 443 // request unless we have entered an error state. 444 // 445 req.on('end', function () { 446 if (!errState) { 447 reverseProxy.end(); 448 } 449 }); 450 451 // If we have been passed buffered data, resume it. 452 if (options.buffer && !errState) { 453 options.buffer.resume(); 454 } 455 }; 456 457 // 458 // ### @private function _forwardRequest (req) 459 // #### @req {ServerRequest} Incoming HTTP Request to proxy. 460 // Forwards the specified `req` to the location specified 461 // by `this.forward` ignoring errors and the subsequent response. 462 // 463 HttpProxy.prototype._forwardRequest = function (req) { 464 var self = this, port, host, outgoing, protocol, forwardProxy; 465 466 port = this.forward.port; 467 host = this.forward.host; 468 469 outgoing = { 470 host: host, 471 port: port, 472 agent: _getAgent(host, port, this.forward.https), 473 method: req.method, 474 path: req.url, 475 headers: req.headers 476 }; 477 478 // Force the `connection` header to be 'close' until 479 // node.js core re-implements 'keep-alive'. 480 outgoing.headers['connection'] = 'close'; 481 482 protocol = _getProtocol(this.forward.https, outgoing); 483 484 // Open new HTTP request to internal resource with will act as a reverse proxy pass 485 forwardProxy = protocol.request(outgoing, function (response) { 486 // 487 // Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. 488 // Remark (indexzero): We will eventually emit a 'forward' event here for performance tuning. 489 // 490 }); 491 492 // Add a listener for the connection timeout event. 493 // 494 // Remark: Ignoring this error in the event 495 // forward target doesn't exist. 496 // 497 forwardProxy.once('error', function (err) { }); 498 499 // Chunk the client request body as chunks from the proxied request come in 500 req.on('data', function (chunk) { 501 forwardProxy.write(chunk); 502 }) 503 504 // At the end of the client request, we are going to stop the proxied request 505 req.on('end', function () { 506 forwardProxy.end(); 507 }); 508 }; 509 510 HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options) { 511 var self = this, outgoing, errState = false, CRLF = '\r\n'; 512 513 // WebSocket requests has method = GET 514 if (req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket') { 515 // This request is not WebSocket request 516 return; 517 } 518 519 // Turn of all bufferings 520 // For server set KeepAlive 521 // For client set encoding 522 function _socket(socket, keepAlive) { 523 socket.setTimeout(0); 524 socket.setNoDelay(true); 525 if (keepAlive) { 526 socket.setKeepAlive(true, 0); 527 } 528 else { 529 socket.setEncoding('utf8'); 530 } 531 } 532 533 function onUpgrade(reverseProxy) { 534 var listeners = {}; 535 536 // We're now connected to the server, so lets change server socket 537 reverseProxy.on('data', listeners._r_data = function(data) { 538 // Pass data to client 539 if (socket.writable) { 540 try { 541 socket.write(data); 542 } 543 catch (e) { 544 socket.end(); 545 reverseProxy.end(); 546 } 547 } 548 }); 549 550 socket.on('data', listeners._data = function(data) { 551 // Pass data from client to server 552 try { 553 reverseProxy.write(data); 554 } 555 catch (e) { 556 reverseProxy.end(); 557 socket.end(); 558 } 559 }); 560 561 // Detach event listeners from reverseProxy 562 function detach() { 563 reverseProxy.removeListener('close', listeners._r_close); 564 reverseProxy.removeListener('data', listeners._r_data); 565 socket.removeListener('data', listeners._data); 566 socket.removeListener('close', listeners._close); 567 } 568 569 // Hook disconnections 570 reverseProxy.on('end', listeners._r_close = function() { 571 socket.end(); 572 detach(); 573 }); 574 575 socket.on('end', listeners._close = function() { 576 reverseProxy.end(); 577 detach(); 578 }); 579 }; 580 581 // Client socket 582 _socket(socket); 583 584 // Remote host address 585 var agent = _getAgent(options.host, options.port), 586 remoteHost = options.host + (options.port - 80 === 0 ? '' : ':' + options.port); 587 588 // Change headers 589 req.headers.host = remoteHost; 590 req.headers.origin = 'http://' + options.host; 591 592 outgoing = { 593 host: options.host, 594 port: options.port, 595 agent: agent, 596 method: 'GET', 597 path: req.url, 598 headers: req.headers 599 }; 600 601 // Make the outgoing WebSocket request 602 var request = http.request(outgoing, function () { }); 603 604 // Not disconnect on update 605 agent.on('upgrade', function(request, remoteSocket, head) { 606 // Prepare socket 607 _socket(remoteSocket, true); 608 609 // Emit event 610 onUpgrade(remoteSocket); 611 }); 612 613 var handshake; 614 if (typeof request.socket !== 'undefined') { 615 request.socket.on('data', handshake = function(data) { 616 // Handshaking 617 618 // Ok, kind of harmfull part of code 619 // Socket.IO is sending hash at the end of handshake 620 // If protocol = 76 621 // But we need to replace 'host' and 'origin' in response 622 // So we split data to printable data and to non-printable 623 // (Non-printable will come after double-CRLF) 624 var sdata = data.toString(); 625 626 // Get Printable 627 sdata = sdata.substr(0, sdata.search(CRLF + CRLF)); 628 629 // Get Non-Printable 630 data = data.slice(Buffer.byteLength(sdata), data.length); 631 632 // Replace host and origin 633 sdata = sdata.replace(remoteHost, options.host) 634 .replace(remoteHost, options.host); 635 636 try { 637 // Write printable 638 socket.write(sdata); 639 640 // Write non-printable 641 socket.write(data); 642 } 643 catch (e) { 644 request.end(); 645 socket.end(); 646 } 647 648 // Catch socket errors 649 socket.on('error', function() { 650 request.end(); 651 }); 652 653 // Remove data listener now that the 'handshake' is complete 654 request.socket.removeListener('data', handshake); 655 }); 656 } 657 658 // Write upgrade-head 659 try { 660 request.write(head); 661 } 662 catch (ex) { 663 request.end(); 664 socket.end(); 665 } 666 667 // If we have been passed buffered data, resume it. 668 if (options.buffer && !errState) { 669 options.buffer.resume(); 670 } 671 };