Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
daguej committed Jul 31, 2013
0 parents commit e76408e
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 0 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
node-proxywrap
==============

This module wraps node's various `Server` interfaces so that they are compatible with the [PROXY protocol](http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt). It automatically parses the PROXY headers and resets `socket.remoteAddress` and `socket.remotePort` so that they have the correct values.

npm install proxywrap

This module is especially useful if you need to get the client IP address when you're behind an AWS ELB in TCP mode.

In HTTP or HTTPS mode (aka SSL termination at ELB), the ELB inserts `X-Forwarded-For` headers for you. However, in TCP mode, the ELB can't understand the underlying protocol, so you lose the client's IP address. With the PROXY protocol and this module, you're able to retain the client IP address with any protocol.

In order for this module to work, you must [enable the PROXY protocol on your ELB](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/enable-proxy-protocol.html) (or whatever proxy your app is behind).

Usage
-----

proxywrap is a drop-in replacement. Here's a simple Express app:

var http = require('http')
, proxiedHttp = require('proxywrap').proxy(http)
, express = require('express')
, app = express()
, srv = proxiedHttp.createServer(app); // instead of http.createServer(app)

app.get('/', function(req, res) {
res.send('IP = ' + req.connection.remoteAddress + ':' + req.connection.remotePort);
});

srv.listen(80);

The magic happens in the `proxywrap.proxy()` call. It wraps the module's `Server` constructor and handles a bunch of messy details for you.

You can do the same with `net` (raw TCP streams), `https`, and `spdy`. It will probably work with other modules that follow the same pattern, but none have been tested.

*Note*: If you're wrapping [node-spdy](https://github.com/indutny/node-spdy), its exports are a little strange:

var proxiedSpdy = require('proxywrap').proxy(require('spdy').server);

**Warning:** *All* traffic to your proxied server MUST use the PROXY protocol. If the first five bytes received aren't `PROXY`, the connection will be dropped. Obviously, the node server accepting PROXY connections should not be exposed directly to the internet; only the proxy (whether ELB, HAProxy, or something else) should be able to connect to node.
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "proxywrap",
"version": "0.1.0",
"description": "Wraps node's Server interfaces to be compatible with the PROXY protocol",
"main": "proxywrap.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/daguej/node-proxywrap"
},
"keywords": [
"proxy",
"elb"
],
"author": "Josh Dague",
"license": "BSD",
"bugs": {
"url": "https://github.com/daguej/node-proxywrap/issues"
}
}
164 changes: 164 additions & 0 deletions proxywrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* node-proxywrap
*
* Copyright (c) 2013, Josh Dague
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

var util = require('util');
//var legacy = !require('stream').Duplex; // TODO: Support <= 0.8 streams interface

// Wraps the given module (ie, http, https, net, tls, etc) interface so that
// `socket.remoteAddress` and `remotePort` work correctly when used with the
// PROXY protocol (http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt)
exports.proxy = function(iface) {
var exports = {};
// copy iface's exports to myself
for (var k in iface) exports[k] = iface[k];


function ProxiedServer(options, requestListener) {
if (!(this instanceof ProxiedServer)) return new ProxiedServer(options, requestListener);

if (typeof options == 'function') {
requestListener = options;
options = null;
}

// iface.Server *requires* an arity of 1; ifaces.Server needs 2
if (options) iface.Server.call(this, options, requestListener);
else iface.Server.call(this, requestListener);

// remove the connection listener attached by iface[s].Server and replace it with our own.
var cl = this.listeners('connection');
this.removeAllListeners('connection');
this.addListener('connection', connectionListener);

// add the old connection listeners to a custom event, which we'll fire after processing the PROXY header
for (var i = 0; i < cl.length; i++) {
this.addListener('proxiedConnection', cl[i]);
}



}
util.inherits(ProxiedServer, iface.Server);

exports.createServer = function(opts, requestListener) {
return new ProxiedServer(opts, requestListener);
}

exports.Server = ProxiedServer;



function connectionListener(socket) {
var self = this, realEmit = socket.emit, history = [];

// TODO: Support <= 0.8 streams interface
//function ondata() {}
//if (legacy) socket.once('data', ondata);

// override the socket's event emitter so we can process data (and discard the PROXY protocol header) before the underlying Server gets it
socket.emit = function(event, data) {
history.push(Array.prototype.slice.call(arguments));
/*if (event == 'data') {
console.log('got a data event :(');
socket.destroy();
} else*/ if (event == 'readable') {
onReadable();
}
}

function restore() {
//if (legacy) socket.removeListener('data', ondata);
// restore normal socket functionality, and fire any events that were emitted while we had control of emit()
socket.emit = realEmit;
for (var i = 0; i < history.length; i++) {
console.log(history[i]);
realEmit.apply(socket, history[i]);
if (history[i][0] == 'end' && socket.onend) socket.onend();
}
history = null;
}

socket.on('readable', onReadable);

var header = '', buf = new Buffer(0);
function onReadable() {
var chunk;
while (null != (chunk = socket.read())) {
buf = Buffer.concat([buf, chunk]);
header += chunk.toString('ascii');

// if the first 5 bytes aren't PROXY, something's not right.
if (header.length >= 5 && header.substr(0, 5) != 'PROXY') return socket.destroy('PROXY protocol error');

var crlf = header.indexOf('\r');
if (crlf > 0) {
socket.removeListener('readable', onReadable);
header = header.substr(0, crlf);

var hlen = header.length;
header = header.split(' ');

Object.defineProperty(socket, 'remoteAddress', {
enumerable: false,
configurable: true,
get: function() {
return header[2];
}
});
Object.defineProperty(socket, 'remotePort', {
enumerable: false,
configurable: true,
get: function() {
return parseInt(header[4], 10);
}
});

// unshifting will fire the readable event
socket.emit = realEmit;
socket.unshift(buf.slice(crlf+2));


self.emit('proxiedConnection', socket);

restore();

if (socket.ondata) {
var data = socket.read();
if (data) socket.ondata(data, 0, data.length);
}

break;

}
else if (header.length > 107) return socket.destroy('PROXY protocol error'); // PROXY header too long
}
}

}

return exports;
}

0 comments on commit e76408e

Please sign in to comment.