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

commit e35884cd37124cff8a26550b1bf0c90cea383aab
Author: Ryan Wolf <rwolf@borderstylo.com>
Date:   Tue, 26 Apr 2011 14:38:59 -0700

Starting anew, to leave out duostack commits.

Diffstat:
ALICENSE | 21+++++++++++++++++++++
AREADME.md | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserver.js | 45+++++++++++++++++++++++++++++++++++++++++++++
Avendor/http-proxy/LICENSE | 24++++++++++++++++++++++++
Avendor/http-proxy/node-http-proxy.js | 672+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/http-proxy/proxy-table.js | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 976 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +paternalistic-twitter-proxy + +Copyright (c) 2011 Ryan Wolf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,57 @@ +Paternalistic Twitter Proxy +== + +Paternatlistic Twitter Proxy intercepts status updates, and refuses to proxy +updates that include location metadata. + +Motivation +=== + +I really like using Seesmic's Twitter client for Android, but I worry the teeny +form factor will make it easy for me to accidentally tweet things I don't intend +(like my location). + +Twitter does a great job of making you check a "include locations" box in your +settings before you can include locations in your tweets, but: + +* I can check that box from my phone in a fit of madness. +* Twitter is free to take away that requirement without notice. + +Install +=== + +On a public-facing server with node 0.4.2 or greater: + + $ node server.js + +Alternate Installation on Duostack +=== + +If you have a beta account at duostack: + + $ gem install duostack + $ duostack create $NAME + $ duostack config stack node-0.4.2 + $ git push duostack master + +The address of your proxy is: http://$NAME.duostack.net + +Setup Seesmic for Android +=== + +Assuming you have Seesmic for Android installed and a Twitter account" + +* Start up Seesmic +* Click on that adorable raccoon head to get the Accounts section +* Click on the plus sign to get to "Add an account" +* Choose "Twitter proxy" +* Enter Twitter username and password +* Uncheck "Use secure connection" +* Enter the address of your proxy in "REST API server" and "Search API server" fields +* Make sure "Use XAuth" is checked +* Scroll back up to the top and click "Sign In" + +License +=== + +Paternalistic Twitter Proxy is licensed under the MIT License. See LICENSE for details. diff --git a/server.js b/server.js @@ -0,0 +1,45 @@ +var httpProxy = require('./vendor/http-proxy/node-http-proxy'), + querystring = require('querystring'); + +var handleTweet = function (req, res, proxy) { + var input = ''; + req.setEncoding('utf8'); + req.on('data', function (chunk) { input += chunk; }); + req.once('end', function () { + var tweet = querystring.parse(input), + blacklist = ['lat', 'long', 'place_id', 'display_coordinates']; + for (var i = 0; i < blacklist.length; i++) { + // if tweet has blacklisted property, reject + if (tweet.hasOwnProperty(blacklist[i])) { + console.log('bad tweet: ' + blacklist[i]); + res.writeHead(400); + res.write('Bad Request'); + return res.end(); + } + } + var options = { + host: 'api.twitter.com', + port: 80, + buffer: { + resume: function () { + req.emit('data', input); + req.emit('end'); + } + } + }; + process.nextTick(function () { + proxy.proxyRequest(req, res, options); + }); + }); +}; + +var handleRest = function (req, res, proxy) { + proxy.proxyRequest(req, res, { host: 'api.twitter.com', post: 80 }); +}; + +httpProxy.createServer(function (req, res, proxy) { + if (req.method == 'POST' && req.url == '/statuses/update.json') { + return handleTweet(req, res, proxy); + } + handleRest(req, res, proxy); +}).listen(80); diff --git a/vendor/http-proxy/LICENSE b/vendor/http-proxy/LICENSE @@ -0,0 +1,23 @@ + + node-http-proxy + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +\ No newline at end of file diff --git a/vendor/http-proxy/node-http-proxy.js b/vendor/http-proxy/node-http-proxy.js @@ -0,0 +1,671 @@ +/* + node-http-proxy.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +var util = require('util'), + http = require('http'), + https = require('https'), + events = require('events'), + ProxyTable = require('./proxy-table').ProxyTable, + maxSockets = 100; + +// +// ### Version 0.5.0 +// +exports.version = [0, 5, 0]; + +// +// ### function _getAgent (host, port, secure) +// #### @host {string} Host of the agent to get +// #### @port {number} Port of the agent to get +// #### @secure {boolean} Value indicating whether or not to use HTTPS +// Retreives an agent from the `http` or `https` module +// and sets the `maxSockets` property appropriately. +// +function _getAgent (host, port, secure) { + var agent = !secure ? http.getAgent(host, port) : https.getAgent({ + host: host, + port: port + }); + + agent.maxSockets = maxSockets; + return agent; +} + +// +// ### function _getProtocol (secure, outgoing) +// #### @secure {Object|boolean} Settings for `https` +// #### @outgoing {Object} Outgoing request options +// Returns the appropriate protocol based on the settings in +// `secure`. If the protocol is `https` this function will update +// the options in `outgoing` as appropriate by adding `ca`, `key`, +// and `cert` if they exist in `secure`. +// +function _getProtocol (secure, outgoing) { + var protocol = secure ? https : http; + + if (typeof secure === 'object') { + outgoing = outgoing || {}; + ['ca', 'cert', 'key'].forEach(function (prop) { + if (secure[prop]) { + outgoing[prop] = secure[prop]; + } + }) + } + + return protocol; +} + +// +// ### function getMaxSockets () +// Returns the maximum number of sockets +// allowed on __every__ outgoing request +// made by __all__ instances of `HttpProxy` +// +exports.getMaxSockets = function () { + return maxSockets; +}; + +// +// ### function setMaxSockets () +// Sets the maximum number of sockets +// allowed on __every__ outgoing request +// made by __all__ instances of `HttpProxy` +// +exports.setMaxSockets = function (value) { + maxSockets = value; +}; + +// +// ### function createServer ([port, host, options, handler]) +// #### @port {number} **Optional** Port to use on the proxy target host. +// #### @host {string} **Optional** Host of the proxy target. +// #### @options {Object} **Optional** Options for the HttpProxy instance used +// #### @handler {function} **Optional** Request handler for the server +// Returns a server that manages an instance of HttpProxy. Flexible arguments allow for: +// +// * `httpProxy.createServer(9000, 'localhost')` +// * `httpProxy.createServer(9000, 'localhost', options) +// * `httpPRoxy.createServer(function (req, res, proxy) { ... })` +// +exports.createServer = function () { + var args = Array.prototype.slice.call(arguments), + callback = typeof args[0] === 'function' && args.shift(), + options = {}, + port, host, forward, silent, proxy, server; + + if (args.length >= 2) { + port = args[0]; + host = args[1]; + options = args[2] || {}; + } + else if (args.length === 1) { + options = args[0] || {}; + if (!options.router && !callback) { + throw new Error('Cannot create server with no router and no callback'); + } + } + + proxy = new HttpProxy(options); + + handler = function (req, res) { + if (callback) { + // + // If we were passed a callback to process the request + // or response in some way, then call it. + // + callback(req, res, proxy); + } + else if (port && host) { + // + // If we have a target host and port for the request + // then proxy to the specified location. + // + proxy.proxyRequest(req, res, { + port: port, + host: host + }); + } + else if (proxy.proxyTable) { + // + // If the proxy is configured with a ProxyTable + // instance then use that before failing. + // + proxy.proxyRequest(req, res); + } + else { + // + // Otherwise this server is improperly configured. + // + throw new Error('Cannot proxy without port, host, or router.') + } + }; + + server = options.https + ? https.createServer(options.https, handler) + : http.createServer(handler); + + server.on('close', function () { + proxy.close(); + }); + + proxy.on('routes', function (routes) { + server.emit('routes', routes); + }); + + if (!callback) { + // WebSocket support: if callback is empty tunnel + // websocket request automatically + server.on('upgrade', function(req, socket, head) { + // Tunnel websocket requests too + + proxy.proxyWebSocketRequest(req, socket, head, { + port: port, + host: host + }); + }); + } + + // + // Set the proxy on the server so it is available + // to the consumer of the server + // + server.proxy = proxy; + + return server; +}; + +// +// ### function HttpProxy (options) +// #### @options {Object} Options for this instance. +// Constructor function for new instances of HttpProxy responsible +// for managing the life-cycle of streaming reverse proxyied HTTP requests. +// +// Example options: +// +// { +// router: { +// 'foo.com': 'localhost:8080', +// 'bar.com': 'localhost:8081' +// }, +// forward: { +// host: 'localhost', +// port: 9001 +// } +// } +// +var HttpProxy = exports.HttpProxy = function (options) { + events.EventEmitter.call(this); + + var self = this; + options = options || {}; + this.forward = options.forward; + this.https = options.https; + + if (options.router) { + this.proxyTable = new ProxyTable(options.router, options.silent, options.hostnameOnly); + this.proxyTable.on('routes', function (routes) { + self.emit('routes', routes); + }); + } +}; + +// Inherit from events.EventEmitter +util.inherits(HttpProxy, events.EventEmitter); + +// +// ### function buffer (obj) +// #### @obj {Object} Object to pause events from +// Buffer `data` and `end` events from the given `obj`. +// Consumers of HttpProxy performing async tasks +// __must__ utilize this utility, to re-emit data once +// the async operation has completed, otherwise these +// __events will be lost.__ +// +// var buffer = httpProxy.buffer(req); +// fs.readFile(path, function(){ +// httpProxy.proxyRequest(req, res, host, port, buffer); +// }); +// +// __Attribution:__ This approach is based heavily on +// [Connect](https://github.com/senchalabs/connect/blob/master/lib/utils.js#L157). +// However, this is not a big leap from the implementation in node-http-proxy < 0.4.0. +// This simply chooses to manage the scope of the events on a new Object literal as opposed to +// [on the HttpProxy instance](https://github.com/nodejitsu/node-http-proxy/blob/v0.3.1/lib/node-http-proxy.js#L154). +// +HttpProxy.prototype.buffer = function (obj) { + var onData, onEnd, events = []; + + obj.on('data', onData = function (data, encoding) { + events.push(['data', data, encoding]); + }); + + obj.on('end', onEnd = function (data, encoding) { + events.push(['end', data, encoding]); + }); + + return { + end: function () { + obj.removeListener('data', onData); + obj.removeListener('end', onEnd); + }, + resume: function () { + this.end(); + for (var i = 0, len = events.length; i < len; ++i) { + obj.emit.apply(obj, events[i]); + } + } + }; +}; + +// +// ### function close () +// Frees the resources associated with this instance, +// if they exist. +// +HttpProxy.prototype.close = function () { + if (this.proxyTable) this.proxyTable.close(); +}; + +// +// ### function proxyRequest (req, res, [port, host, paused]) +// #### @req {ServerRequest} Incoming HTTP Request to proxy. +// #### @res {ServerResponse} Outgoing HTTP Request to write proxied data to. +// #### @options {Object} Options for the outgoing proxy request. +// options.port {number} Port to use on the proxy target host. +// options.host {string} Host of the proxy target. +// options.buffer {Object} Result from `httpProxy.buffer(req)` +// options.https {Object|boolean} Settings for https. +// +HttpProxy.prototype.proxyRequest = function (req, res, options) { + var self = this, errState = false, location, outgoing, protocol, reverseProxy; + + // Create an empty options hash if none is passed. + options = options || {}; + + // + // Check the proxy table for this instance to see if we need + // to get the proxy location for the request supplied. We will + // always ignore the proxyTable if an explicit `port` and `host` + // arguments are supplied to `proxyRequest`. + // + if (this.proxyTable && !options.host) { + location = this.proxyTable.getProxyLocation(req); + + // + // If no location is returned from the ProxyTable instance + // then respond with `404` since we do not have a valid proxy target. + // + if (!location) { + res.writeHead(404); + return res.end(); + } + + // + // When using the ProxyTable in conjunction with an HttpProxy instance + // only the following arguments are valid: + // + // * `proxy.proxyRequest(req, res, { host: 'localhost' })`: This will be skipped + // * `proxy.proxyRequest(req, res, { buffer: buffer })`: Buffer will get updated appropriately + // * `proxy.proxyRequest(req, res)`: Options will be assigned appropriately. + // + options.port = location.port; + options.host = location.host; + } + + // + // Add `x-forwarded-for` header to availible client IP to apps behind proxy + // + req.headers['x-forwarded-for'] = req.connection.remoteAddress; + + // + // Emit the `start` event indicating that we have begun the proxy operation. + // + this.emit('start', req, res, options); + + // + // If forwarding is enabled for this instance, foward proxy the + // specified request to the address provided in `this.forward` + // + 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; + res.writeHead(500, { 'Content-Type': 'text/plain' }); + + if (req.method !== 'HEAD') { + res.write('An error has occurred: ' + JSON.stringify(err)); + } + + res.end(); + } + + outgoing = { + host: options.host, + port: options.port, + agent: _getAgent(options.host, options.port, options.https || this.https), + method: req.method, + path: req.url, + headers: req.headers + }; + + // Force the `connection` header to be 'close' until + // node.js core re-implements 'keep-alive'. + outgoing.headers['connection'] = 'close'; + + protocol = _getProtocol(options.https || this.https, outgoing); + + // Open new HTTP request to internal resource with will act as a reverse proxy pass + reverseProxy = protocol.request(outgoing, function (response) { + + // Process the `reverseProxy` `response` when it's received. + if (response.headers.connection) { + if (req.headers.connection) response.headers.connection = req.headers.connection; + else response.headers.connection = 'close'; + } + + // Set the headers of the client response + res.writeHead(response.statusCode, response.headers); + + // `response.statusCode === 304`: No 'data' event and no 'end' + if (response.statusCode === 304) { + return res.end(); + } + + // For each data `chunk` received from the `reverseProxy` + // `response` write it to the outgoing `res`. + response.on('data', function (chunk) { + if (req.method !== 'HEAD') { + res.write(chunk); + } + }); + + // 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. + response.on('end', function () { + if (!errState) { + reverseProxy.removeListener('error', proxyError); + res.end(); + + // Emit the `end` event now that we have completed proxying + self.emit('end', req, res); + } + }); + }); + + // Handle 'error' events from the `reverseProxy`. + reverseProxy.once('error', proxyError); + + // For each data `chunk` received from the incoming + // `req` write it to the `reverseProxy` request. + req.on('data', function (chunk) { + if (!errState) { + reverseProxy.write(chunk); + } + }); + + // + // 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(); + } + }); + + // If we have been passed buffered data, resume it. + if (options.buffer && !errState) { + options.buffer.resume(); + } +}; + +// +// ### @private function _forwardRequest (req) +// #### @req {ServerRequest} Incoming HTTP Request to proxy. +// Forwards the specified `req` to the location specified +// by `this.forward` ignoring errors and the subsequent response. +// +HttpProxy.prototype._forwardRequest = function (req) { + var self = this, port, host, outgoing, protocol, forwardProxy; + + port = this.forward.port; + host = this.forward.host; + + outgoing = { + host: host, + port: port, + agent: _getAgent(host, port, this.forward.https), + method: req.method, + path: req.url, + headers: req.headers + }; + + // Force the `connection` header to be 'close' until + // node.js core re-implements 'keep-alive'. + outgoing.headers['connection'] = 'close'; + + protocol = _getProtocol(this.forward.https, outgoing); + + // Open new HTTP request to internal resource with will act as a reverse proxy pass + forwardProxy = protocol.request(outgoing, function (response) { + // + // Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy. + // Remark (indexzero): We will eventually emit a 'forward' event here for performance tuning. + // + }); + + // Add a listener for the connection timeout event. + // + // Remark: Ignoring this error in the event + // forward target doesn't exist. + // + forwardProxy.once('error', function (err) { }); + + // Chunk the client request body as chunks from the proxied request come in + req.on('data', function (chunk) { + forwardProxy.write(chunk); + }) + + // At the end of the client request, we are going to stop the proxied request + req.on('end', function () { + forwardProxy.end(); + }); +}; + +HttpProxy.prototype.proxyWebSocketRequest = function (req, socket, head, options) { + var self = this, outgoing, errState = false, CRLF = '\r\n'; + + // WebSocket requests has method = GET + if (req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket') { + // This request is not WebSocket request + return; + } + + // Turn of all bufferings + // For server set KeepAlive + // For client set encoding + function _socket(socket, keepAlive) { + socket.setTimeout(0); + socket.setNoDelay(true); + if (keepAlive) { + socket.setKeepAlive(true, 0); + } + else { + socket.setEncoding('utf8'); + } + } + + function onUpgrade(reverseProxy) { + var listeners = {}; + + // We're now connected to the server, so lets change server socket + reverseProxy.on('data', listeners._r_data = function(data) { + // Pass data to client + if (socket.writable) { + try { + socket.write(data); + } + catch (e) { + socket.end(); + reverseProxy.end(); + } + } + }); + + socket.on('data', listeners._data = function(data) { + // Pass data from client to server + try { + reverseProxy.write(data); + } + catch (e) { + reverseProxy.end(); + socket.end(); + } + }); + + // Detach event listeners from reverseProxy + function detach() { + reverseProxy.removeListener('close', listeners._r_close); + reverseProxy.removeListener('data', listeners._r_data); + socket.removeListener('data', listeners._data); + socket.removeListener('close', listeners._close); + } + + // Hook disconnections + reverseProxy.on('end', listeners._r_close = function() { + socket.end(); + detach(); + }); + + socket.on('end', listeners._close = function() { + reverseProxy.end(); + detach(); + }); + }; + + // Client socket + _socket(socket); + + // Remote host address + var agent = _getAgent(options.host, options.port), + remoteHost = options.host + (options.port - 80 === 0 ? '' : ':' + options.port); + + // Change headers + req.headers.host = remoteHost; + req.headers.origin = 'http://' + options.host; + + outgoing = { + host: options.host, + port: options.port, + agent: agent, + method: 'GET', + path: req.url, + headers: req.headers + }; + + // Make the outgoing WebSocket request + var request = http.request(outgoing, function () { }); + + // Not disconnect on update + agent.on('upgrade', function(request, remoteSocket, head) { + // Prepare socket + _socket(remoteSocket, true); + + // Emit event + onUpgrade(remoteSocket); + }); + + var handshake; + if (typeof request.socket !== 'undefined') { + request.socket.on('data', handshake = function(data) { + // Handshaking + + // Ok, kind of harmfull part of code + // Socket.IO is sending hash at the end of handshake + // If protocol = 76 + // But we need to replace 'host' and 'origin' in response + // So we split data to printable data and to non-printable + // (Non-printable will come after double-CRLF) + var sdata = data.toString(); + + // Get Printable + sdata = sdata.substr(0, sdata.search(CRLF + CRLF)); + + // Get Non-Printable + data = data.slice(Buffer.byteLength(sdata), data.length); + + // Replace host and origin + sdata = sdata.replace(remoteHost, options.host) + .replace(remoteHost, options.host); + + try { + // Write printable + socket.write(sdata); + + // Write non-printable + socket.write(data); + } + catch (e) { + request.end(); + socket.end(); + } + + // Catch socket errors + socket.on('error', function() { + request.end(); + }); + + // Remove data listener now that the 'handshake' is complete + request.socket.removeListener('data', handshake); + }); + } + + // Write upgrade-head + try { + request.write(head); + } + catch (ex) { + request.end(); + socket.end(); + } + + // If we have been passed buffered data, resume it. + if (options.buffer && !errState) { + options.buffer.resume(); + } +}; +\ No newline at end of file diff --git a/vendor/http-proxy/proxy-table.js b/vendor/http-proxy/proxy-table.js @@ -0,0 +1,156 @@ +/* + node-http-proxy.js: Lookup table for proxy targets in node.js + + Copyright (c) 2010 Charlie Robbins + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +var util = require('util'), + events = require('events'), + fs = require('fs'); + +// +// ### function ProxyTable (router, silent) +// #### @router {Object} Object containing the host based routes +// #### @silent {Boolean} Value indicating whether we should suppress logs +// #### @hostnameOnly {Boolean} Value indicating if we should route based on __hostname string only__ +// Constructor function for the ProxyTable responsible for getting +// locations of proxy targets based on ServerRequest headers; specifically +// the HTTP host header. +// +var ProxyTable = exports.ProxyTable = function (router, silent, hostnameOnly) { + events.EventEmitter.call(this); + + this.silent = typeof silent !== 'undefined' ? silent : true; + this.hostnameOnly = typeof hostnameOnly !== 'undefined' ? hostnameOnly : false; + + if (typeof router === 'object') { + // + // If we are passed an object literal setup + // the routes with RegExps from the router + // + this.setRoutes(router); + } + else if (typeof router === 'string') { + // + // If we are passed a string then assume it is a + // file path, parse that file and watch it for changes + // + var self = this; + this.routeFile = router; + this.setRoutes(JSON.parse(fs.readFileSync(router)).router); + + fs.watchFile(this.routeFile, function () { + fs.readFile(self.routeFile, function (err, data) { + if (err) throw err; + self.setRoutes(JSON.parse(data).router); + self.emit('routes', self.hostnameOnly === false ? self.routes : self.router); + }); + }); + } + else { + throw new Error('Cannot parse router with unknown type: ' + typeof router); + } +}; + +// Inherit from events.EventEmitter +util.inherits(ProxyTable, events.EventEmitter); + +// +// ### function setRoutes (router) +// #### @router {Object} Object containing the host based routes +// Sets the host-based routes to be used by this instance. +// +ProxyTable.prototype.setRoutes = function (router) { + if (!router) throw new Error('Cannot update ProxyTable routes without router.'); + + this.router = router; + + if (this.hostnameOnly === false) { + var self = this; + this.routes = []; + + Object.keys(router).forEach(function (path) { + var route = new RegExp(path, 'i'); + + self.routes.push({ + route: route, + target: router[path] + }); + }); + } +}; + +// +// ### function getProxyLocation (req) +// #### @req {ServerRequest} The incoming server request to get proxy information about. +// Returns the proxy location based on the HTTP Headers in the ServerRequest `req` +// available to this instance. +// +ProxyTable.prototype.getProxyLocation = function (req) { + if (!req || !req.headers || !req.headers.host) { + return null; + } + + var target = req.headers.host.split(':')[0]; + if (this.hostnameOnly == true) { + if (this.router.hasOwnProperty(target)) { + var location = this.router[target].split(':'), + host = location[0], + port = location.length === 1 ? 80 : location[1]; + + return { + port: port, + host: host + }; + } + } + else { + target += req.url; + for (var i in this.routes) { + var match, route = this.routes[i]; + if (match = target.match(route.route)) { + var location = route.target.split(':'), + host = location[0], + port = location.length === 1 ? 80 : location[1]; + + return { + port: port, + host: host + }; + } + } + } + + return null; +}; + +// +// ### close function () +// Cleans up the event listeneners maintained +// by this instance. +// +ProxyTable.prototype.close = function () { + if (typeof this.routeFile === 'string') { + fs.unwatchFile(this.routeFile); + } +}; +\ No newline at end of file