Skip to content

Commit e76408e

Browse files
committed
Initial commit
0 parents  commit e76408e

3 files changed

Lines changed: 225 additions & 0 deletions

File tree

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
node-proxywrap
2+
==============
3+
4+
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.
5+
6+
npm install proxywrap
7+
8+
This module is especially useful if you need to get the client IP address when you're behind an AWS ELB in TCP mode.
9+
10+
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.
11+
12+
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).
13+
14+
Usage
15+
-----
16+
17+
proxywrap is a drop-in replacement. Here's a simple Express app:
18+
19+
var http = require('http')
20+
, proxiedHttp = require('proxywrap').proxy(http)
21+
, express = require('express')
22+
, app = express()
23+
, srv = proxiedHttp.createServer(app); // instead of http.createServer(app)
24+
25+
app.get('/', function(req, res) {
26+
res.send('IP = ' + req.connection.remoteAddress + ':' + req.connection.remotePort);
27+
});
28+
29+
srv.listen(80);
30+
31+
The magic happens in the `proxywrap.proxy()` call. It wraps the module's `Server` constructor and handles a bunch of messy details for you.
32+
33+
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.
34+
35+
*Note*: If you're wrapping [node-spdy](https://github.com/indutny/node-spdy), its exports are a little strange:
36+
37+
var proxiedSpdy = require('proxywrap').proxy(require('spdy').server);
38+
39+
**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.

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "proxywrap",
3+
"version": "0.1.0",
4+
"description": "Wraps node's Server interfaces to be compatible with the PROXY protocol",
5+
"main": "proxywrap.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/daguej/node-proxywrap"
12+
},
13+
"keywords": [
14+
"proxy",
15+
"elb"
16+
],
17+
"author": "Josh Dague",
18+
"license": "BSD",
19+
"bugs": {
20+
"url": "https://github.com/daguej/node-proxywrap/issues"
21+
}
22+
}

proxywrap.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* node-proxywrap
3+
*
4+
* Copyright (c) 2013, Josh Dague
5+
* All rights reserved.
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice, this
11+
* list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20+
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23+
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
28+
var util = require('util');
29+
//var legacy = !require('stream').Duplex; // TODO: Support <= 0.8 streams interface
30+
31+
// Wraps the given module (ie, http, https, net, tls, etc) interface so that
32+
// `socket.remoteAddress` and `remotePort` work correctly when used with the
33+
// PROXY protocol (http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt)
34+
exports.proxy = function(iface) {
35+
var exports = {};
36+
// copy iface's exports to myself
37+
for (var k in iface) exports[k] = iface[k];
38+
39+
40+
function ProxiedServer(options, requestListener) {
41+
if (!(this instanceof ProxiedServer)) return new ProxiedServer(options, requestListener);
42+
43+
if (typeof options == 'function') {
44+
requestListener = options;
45+
options = null;
46+
}
47+
48+
// iface.Server *requires* an arity of 1; ifaces.Server needs 2
49+
if (options) iface.Server.call(this, options, requestListener);
50+
else iface.Server.call(this, requestListener);
51+
52+
// remove the connection listener attached by iface[s].Server and replace it with our own.
53+
var cl = this.listeners('connection');
54+
this.removeAllListeners('connection');
55+
this.addListener('connection', connectionListener);
56+
57+
// add the old connection listeners to a custom event, which we'll fire after processing the PROXY header
58+
for (var i = 0; i < cl.length; i++) {
59+
this.addListener('proxiedConnection', cl[i]);
60+
}
61+
62+
63+
64+
}
65+
util.inherits(ProxiedServer, iface.Server);
66+
67+
exports.createServer = function(opts, requestListener) {
68+
return new ProxiedServer(opts, requestListener);
69+
}
70+
71+
exports.Server = ProxiedServer;
72+
73+
74+
75+
function connectionListener(socket) {
76+
var self = this, realEmit = socket.emit, history = [];
77+
78+
// TODO: Support <= 0.8 streams interface
79+
//function ondata() {}
80+
//if (legacy) socket.once('data', ondata);
81+
82+
// override the socket's event emitter so we can process data (and discard the PROXY protocol header) before the underlying Server gets it
83+
socket.emit = function(event, data) {
84+
history.push(Array.prototype.slice.call(arguments));
85+
/*if (event == 'data') {
86+
console.log('got a data event :(');
87+
socket.destroy();
88+
} else*/ if (event == 'readable') {
89+
onReadable();
90+
}
91+
}
92+
93+
function restore() {
94+
//if (legacy) socket.removeListener('data', ondata);
95+
// restore normal socket functionality, and fire any events that were emitted while we had control of emit()
96+
socket.emit = realEmit;
97+
for (var i = 0; i < history.length; i++) {
98+
console.log(history[i]);
99+
realEmit.apply(socket, history[i]);
100+
if (history[i][0] == 'end' && socket.onend) socket.onend();
101+
}
102+
history = null;
103+
}
104+
105+
socket.on('readable', onReadable);
106+
107+
var header = '', buf = new Buffer(0);
108+
function onReadable() {
109+
var chunk;
110+
while (null != (chunk = socket.read())) {
111+
buf = Buffer.concat([buf, chunk]);
112+
header += chunk.toString('ascii');
113+
114+
// if the first 5 bytes aren't PROXY, something's not right.
115+
if (header.length >= 5 && header.substr(0, 5) != 'PROXY') return socket.destroy('PROXY protocol error');
116+
117+
var crlf = header.indexOf('\r');
118+
if (crlf > 0) {
119+
socket.removeListener('readable', onReadable);
120+
header = header.substr(0, crlf);
121+
122+
var hlen = header.length;
123+
header = header.split(' ');
124+
125+
Object.defineProperty(socket, 'remoteAddress', {
126+
enumerable: false,
127+
configurable: true,
128+
get: function() {
129+
return header[2];
130+
}
131+
});
132+
Object.defineProperty(socket, 'remotePort', {
133+
enumerable: false,
134+
configurable: true,
135+
get: function() {
136+
return parseInt(header[4], 10);
137+
}
138+
});
139+
140+
// unshifting will fire the readable event
141+
socket.emit = realEmit;
142+
socket.unshift(buf.slice(crlf+2));
143+
144+
145+
self.emit('proxiedConnection', socket);
146+
147+
restore();
148+
149+
if (socket.ondata) {
150+
var data = socket.read();
151+
if (data) socket.ondata(data, 0, data.length);
152+
}
153+
154+
break;
155+
156+
}
157+
else if (header.length > 107) return socket.destroy('PROXY protocol error'); // PROXY header too long
158+
}
159+
}
160+
161+
}
162+
163+
return exports;
164+
}

0 commit comments

Comments
 (0)