paternalistic-twitter-proxy

Twitter proxy that refuses status updates with location metadata.
git clone https://wehaveforgeathome.hates.computer/paternalistic-twitter-proxy.git
Log | Files | Refs | LICENSE

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