Skip to content

Commit 7249ef3

Browse files
committed
WebSocket proxy support, fixed 304 code halting
1 parent 711258e commit 7249ef3

File tree

2 files changed

+255
-17
lines changed

2 files changed

+255
-17
lines changed

lib/node-http-proxy.js

+204-17
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2424
2525
*/
26-
26+
2727
var sys = require('sys'),
2828
http = require('http'),
2929
events = require('events'),
3030
pool = require('pool'),
31-
min = 0,
31+
min = 0,
3232
max = 100;
3333

3434
// Setup the PoolManager
@@ -42,24 +42,28 @@ exports.createServer = function () {
4242
callback = typeof args[args.length - 1] === 'function' && args.pop();
4343
if (args[0]) port = args[0];
4444
if (args[1]) host = args[1];
45-
45+
4646
var server = http.createServer(function (req, res){
4747
var proxy = new HttpProxy(req, res);
48-
48+
4949
proxy.emitter.on('proxy', function (err, body) {
5050
server.emit('proxy', err, body);
5151
});
52-
52+
5353
// If we were passed a callback to process the request
5454
// or response in some way, then call it.
5555
if(callback) {
5656
callback(req, res, proxy);
5757
}
58-
else {
58+
else {
5959
proxy.proxyRequest(port, server);
6060
}
6161
});
62-
62+
63+
// WebSocket support
64+
server.on('update', function() {
65+
});
66+
6367
return server;
6468
};
6569

@@ -73,12 +77,21 @@ exports.setMax = function (value) {
7377
manager.setMaxClients(max);
7478
};
7579

76-
var HttpProxy = function (req, res) {
80+
var HttpProxy = function (req, res, /* optional */ head) {
7781
this.emitter = new(events.EventEmitter);
7882
this.events = {};
7983
this.req = req;
80-
this.res = res;
81-
this.watch(req);
84+
// If this request is upgrade request
85+
// No response will be passed
86+
if (!req.headers.upgrade) {
87+
this.res = res;
88+
this.watch(req);
89+
} else {
90+
// Second argument will be socket
91+
this.sock = res;
92+
this.head = head;
93+
this.watch(res);
94+
}
8295
};
8396

8497
HttpProxy.prototype = {
@@ -90,7 +103,7 @@ HttpProxy.prototype = {
90103
}
91104
return arr;
92105
},
93-
106+
94107
watch: function (req) {
95108
this.events = [];
96109
var self = this;
@@ -105,11 +118,11 @@ HttpProxy.prototype = {
105118
req.addListener('data', this.onData);
106119
req.addListener('end', this.onEnd);
107120
},
108-
121+
109122
unwatch: function (req) {
110123
req.removeListener('data', this.onData);
111124
req.removeListener('end', this.onEnd);
112-
125+
113126
// Rebroadcast any events that have been buffered
114127
for (var i = 0, len = this.events.length; i < len; ++i) {
115128
req.emit.apply(req, this.events[i]);
@@ -120,13 +133,13 @@ HttpProxy.prototype = {
120133
// Remark: nodeProxy.body exists solely for testability
121134
var self = this, req = this.req, res = this.res;
122135
self.body = '';
123-
136+
124137
// Open new HTTP request to internal resource with will act as a reverse proxy pass
125138
var p = manager.getPool(port, server);
126-
139+
127140
p.on('error', function (err) {
128141
// Remark: We should probably do something here
129-
// but this is a hot-fix because I don't think 'pool'
142+
// but this is a hot-fix because I don't think 'pool'
130143
// should be emitting this event.
131144
});
132145

@@ -141,7 +154,7 @@ HttpProxy.prototype = {
141154

142155
res.end();
143156
};
144-
157+
145158
// Add a listener for the connection timeout event
146159
reverse_proxy.addListener('error', error);
147160

@@ -155,6 +168,13 @@ HttpProxy.prototype = {
155168
// Set the response headers of the client response
156169
res.writeHead(response.statusCode, response.headers);
157170

171+
// Status code = 304
172+
// No 'data' event and no 'end'
173+
if (response.statusCode === 304) {
174+
res.end();
175+
return;
176+
}
177+
158178
// Add event handler for the proxied response in chunks
159179
response.addListener('data', function (chunk) {
160180
if(req.method !== 'HEAD') {
@@ -184,6 +204,173 @@ HttpProxy.prototype = {
184204

185205
self.unwatch(req);
186206
});
207+
},
208+
209+
/**
210+
* WebSocket Tunnel realization
211+
* Copyright (c) 2010 Fedor Indutny : http://github.com/donnerjack13589
212+
*/
213+
proxyWebSocketRequest: function (port, server, host /* optional */) {
214+
var self = this, update = self.update, req = self.req, socket = self.sock,
215+
head = self.head, headers = new _headers(req.headers), CRLF = '\r\n',
216+
listeners = {};
217+
218+
// Will generate clone of headers
219+
// To not change original
220+
function _headers(headers) {
221+
var h = {};
222+
for (var i in headers) {
223+
h[i] = headers[i];
224+
}
225+
return h;
226+
}
227+
228+
// WebSocket requests has
229+
// method = GET
230+
if (req.method !== 'GET' || headers.upgrade.toLowerCase() !== 'websocket') {
231+
// This request is not WebSocket request
232+
return;
233+
}
234+
235+
// Turn of all bufferings
236+
// For server set KeepAlive
237+
// For client set encoding
238+
function _socket(socket, server) {
239+
socket.setTimeout(0);
240+
socket.setNoDelay(true);
241+
if (server) {
242+
socket.setKeepAlive(true, 0);
243+
} else {
244+
socket.setEncoding('utf8');
245+
}
246+
}
247+
248+
// Client socket
249+
_socket(socket);
250+
251+
// If host is undefined
252+
// Get it from headers
253+
if (!host) {
254+
host = headers.Host;
255+
}
256+
// Remote host address
257+
var remote_host = server + (port - 80 === 0 ? '' : ':' + port);
258+
259+
// Change headers
260+
headers.Host = remote_host;
261+
headers.Origin = 'http://' + remote_host;
262+
263+
// Open request
264+
var p = manager.getPool(port, server);
265+
266+
p.getClient(function(client) {
267+
// Based on 'pool/main.js'
268+
var request = client.request('GET', req.url, headers);
269+
270+
var errorListener = function (error) {
271+
p.emit('error', error);
272+
request.emit('error', error);
273+
socket.end();
274+
}
275+
276+
// Not disconnect on update
277+
client.on('upgrade', function(request, remote_socket, head) {
278+
// Prepare socket
279+
_socket(remote_socket, true);
280+
281+
// Emit event
282+
onUpgrade(remote_socket);
283+
});
284+
285+
client.on('error', errorListener);
286+
request.on('response', function (response) {
287+
response.on('end', function () {
288+
client.removeListener('error', errorListener);
289+
client.busy = false;
290+
p.onFree(client);
291+
})
292+
})
293+
client.busy = true;
294+
295+
var t;
296+
request.socket.on('data', t = function(data) {
297+
// Handshaking
298+
299+
// Ok, kind of harmfull part of code
300+
// Socket.IO is sending hash at the end of handshake
301+
// If protocol = 76
302+
// But we need to replace 'host' and 'origin' in response
303+
// So we split data to printable data and to non-printable
304+
// (Non-printable will come after double-CRLF)
305+
var sdata = data.toString();
306+
307+
// Get Printable
308+
sdata = sdata
309+
.substr(0, sdata.search(CRLF + CRLF));
310+
311+
// Get Non-Printable
312+
data = data.slice(Buffer.byteLength(sdata), data.length);
313+
314+
// Replace host and origin
315+
sdata = sdata
316+
.replace(remote_host, host)
317+
.replace(remote_host, host);
318+
319+
// Write printable
320+
socket.write(sdata);
321+
322+
// Write non-printable
323+
socket.write(data);
324+
325+
// Remove data listener
326+
request.socket.removeListener('data', t);
327+
});
328+
329+
// Write upgrade-head
330+
request.write(head);
331+
self.unwatch(socket);
332+
});
333+
334+
// Request
335+
336+
function onUpgrade(reverse_proxy) {
337+
// We're now connected to the server
338+
// So lets change server socket
339+
340+
reverse_proxy.on('data', listeners._r_data = function(data) {
341+
// Pass data to client
342+
if (socket.writable) {
343+
socket.write(data);
344+
}
345+
});
346+
347+
socket.on('data', listeners._data = function(data){
348+
// Pass data from client to server
349+
// Socket thougth that it isn't writable
350+
reverse_proxy.write(data);
351+
});
352+
353+
// Detach event listeners from reverse_proxy
354+
function detach() {
355+
reverse_proxy.removeListener('close', listeners._r_close);
356+
reverse_proxy.removeListener('data', listeners._r_data);
357+
socket.removeListener('data', listeners._data);
358+
socket.removeListener('close', listeners._close);
359+
}
360+
361+
// Hook disconnections
362+
reverse_proxy.on('close', listeners._r_close = function() {
363+
socket.end();
364+
detach();
365+
});
366+
367+
socket.on('close', listeners._close = function() {
368+
reverse_proxy.end();
369+
detach();
370+
});
371+
372+
};
373+
187374
}
188375
};
189376

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
node-http-proxy-websocket-test.js: http proxy for node.js
3+
4+
Copyright (c) 2010 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 vows = require('vows'),
28+
sys = require('sys'),
29+
assert = require('assert'),
30+
http = require('http');
31+
32+
var httpProxy = require('./../lib/node-http-proxy');
33+
var testServers = {};
34+
35+
var server = httpProxy.createServer(function(req, res) {
36+
var p = new httpProxy.HttpProxy(req, res);
37+
38+
sys.puts('http request');
39+
40+
p.proxyRequest(8080, 'localhost');
41+
});
42+
43+
server.on('upgrade', function(req, socket, head) {
44+
var p = new httpProxy.HttpProxy(req, socket, head);
45+
46+
sys.puts('websocket request');
47+
48+
p.proxyWebSocketRequest(8080, 'localhost');
49+
});
50+
51+
server.listen(80);

0 commit comments

Comments
 (0)