| | /*
node-http-proxy.js: http proxy for node.js
Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Marak Squires, Fedor Indutny
@@ -26,18 +26,48 @@
var util = require('util'),
http = require('http'),
+ https = require('https'),
events = require('events'),
ProxyTable = require('./proxy-table').ProxyTable,
- maxSockets = 100; |
Version 0.4.2 | exports.version = [0, 4, 2]; |
function _getAgent (host, port)
+ 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
-Retreives an agent from the http module
-and sets the maxSockets property appropriately. | function _getAgent (host, port) { |
| TODO (indexzero): Make this configurable for http / https | var agent = http.getAgent(host, port);
+@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
@@ -67,11 +97,10 @@ made by all instances of HttpProxy
`httpProxy.createServer(9000, 'localhost', options)
httpPRoxy.createServer(function (req, res, proxy) { ... })
| exports.createServer = function () {
- var args, callback, port, host, forward,
- silent, options, proxy, server;
-
- args = Array.prototype.slice.call(arguments);
- callback = typeof args[args.length - 1] === 'function' && args.pop();
+ 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];
@@ -86,21 +115,27 @@ made by all instances of HttpProxy
}
proxy = new HttpProxy(options);
- server = http.createServer(function (req, res) {
- proxy.emit('request', req, request.headers.host, req.url); |
| If we were passed a callback to process the request
-or response in some way, then call it. | if (callback) {
- callback(req, res, proxy);
+
+ 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) {
- proxy.proxyRequest(req, res, port, host);
+ 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) {
- proxy.proxyRequest(req, res);
+ else if (proxy.proxyTable) { |
| If the proxy is configured with a ProxyTable
+instance then use that before failing. | proxy.proxyRequest(req, res);
}
- else {
- throw new Error('Cannot proxy without port, host, or router.')
+ 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();
@@ -110,14 +145,19 @@ or response in some way, then call it. | 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(port, host);
+ 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)
+}; |
function HttpProxy (options)
@options {Object} Options for this instance.
@@ -139,17 +179,18 @@ for managing the life-cycle of streaming reverse proxyied HTTP requests.
| var HttpProxy = exports.HttpProxy = function (options) {
events.EventEmitter.call(this);
- options = options || {};
- this.options = options;
+ var self = this;
+ options = options || {};
+ this.forward = options.forward;
+ this.https = options.https;
if (options.router) {
- var self = this;
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)
+}; |
| Inherit from events.EventEmitter | util.inherits(HttpProxy, events.EventEmitter); |
function buffer (obj)
@obj {Object} Object to pause events from
@@ -167,7 +208,7 @@ the async operation has completed, otherwise these
Attribution: This approach is based heavily on
Connect.
-However, this is not a big leap from the implementation in node-http-proxy < 0.4.0.
+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. | HttpProxy.prototype.buffer = function (obj) {
var onData, onEnd, events = [];
@@ -192,51 +233,54 @@ This simply chooses to manage the scope of the events on a new Object literal a
}
}
};
-}; |
function close ()
+}; |
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])
+}; |
function proxyRequest (req, res, [port, host, paused])
@req {ServerRequest} Incoming HTTP Request to proxy.
@res {ServerResponse} Outgoing HTTP Request to write proxied data to.
-@port {number} Optional Port to use on the proxy target host.
+@options {Object} Options for the outgoing proxy request.
-@host {string} Optional Host of the proxy target.
-
-@buffer {Object} Optional Result from httpProxy.buffer(req) | HttpProxy.prototype.proxyRequest = function (req, res, port, host, buffer) {
- var self = this, reverseProxy, location, errState = false, opts;
- |
| Check the proxy table for this instance to see if we need
+ 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 && !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
+ |
| 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
+ |
| When using the ProxyTable in conjunction with an HttpProxy instance
only the following arguments are valid:
-proxy.proxyRequest(req, res, port, host, buffer): This will be skipped
-proxy.proxyRequest(req, res, buffer): Buffer will get updated appropriately
-proxy.proxyRequest(req, res): No effect undefined = undefined
- | buffer = port;
- port = location.port;
- host = location.host;
+ 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;
}
- |
| Emit the start event indicating that we have begun the proxy operation. | this.emit('start', req, res, host, port);
- |
| If forwarding is enabled for this instance, foward proxy the
-specified request to the address provided in this.options.forward | if (this.options.forward) {
- this.emit('forward', req, res, this.options.forward.host, this.options.forward.port);
+ |
| 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)
+ |
function proxyError (err)
@err {Error} Error contacting the proxy target
@@ -252,28 +296,30 @@ contacting the proxy target at host / port.
res.end();
}
- var opts = {
- host: host,
- port: port,
- agent: _getAgent(host, port),
+ 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'. | opts.headers['connection'] = 'close';
- |
| Open new HTTP request to internal resource with will act as a reverse proxy pass | reverseProxy = http.request(opts, function (response) {
- |
| Process the reverseProxy response when it's received. | if (response.headers.connection) {
+ |
| 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) {
+ } |
| 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
+ } |
| 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
+ }); |
| 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
@@ -281,164 +327,174 @@ removed. | 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
+ |
| 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
+ }); |
| 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 (buffer && !errState) {
- buffer.resume();
+ }); |
| If we have been passed buffered data, resume it. | if (options.buffer && !errState) {
+ options.buffer.resume();
}
};
- |
@private function _forwardRequest (req)
+ |
@private function _forwardRequest (req)
@req {ServerRequest} Incoming HTTP Request to proxy.
Forwards the specified req to the location specified
-by this.options.forward ignoring errors and the subsequent response. | HttpProxy.prototype._forwardRequest = function (req) {
- var self = this, port, host, forwardProxy, opts;
+by this.forward ignoring errors and the subsequent response. | HttpProxy.prototype._forwardRequest = function (req) {
+ var self = this, port, host, outgoing, protocol, forwardProxy;
- port = this.options.forward.port;
- host = this.options.forward.host;
+ port = this.forward.port;
+ host = this.forward.host;
- opts = {
+ outgoing = {
host: host,
port: port,
- agent: _getAgent(host, 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'. | opts.headers['connection'] = 'close';
- |
| Open new HTTP request to internal resource with will act as a reverse proxy pass | forwardProxy = http.request(opts, function (response) { |
| Ignore the response from the forward proxy since this is a 'fire-and-forget' proxy.
+ |
| 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.
+ |
| 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) {
+ 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 () {
+ }) |
| 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 (port, server, host, data) {
- var self = this, req = self.req, socket = self.sock, head = self.head,
- headers = new _headers(req.headers), CRLF = '\r\n'; |
| Will generate clone of headers
-To not change original | function _headers(headers) {
- var h = {};
- for (var i in headers) {
- h[i] = headers[i];
- }
- return h;
- } |
| WebSocket requests has method = GET | if (req.method !== 'GET' || headers.upgrade.toLowerCase() !== 'websocket') { |
| This request is not WebSocket request | |
| Turn of all bufferings
+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 | |
| Turn of all bufferings
For server set KeepAlive
-For client set encoding | function _socket(socket, server) {
+For client set encoding | function _socket(socket, keepAlive) {
socket.setTimeout(0);
socket.setNoDelay(true);
- if (server) {
+ if (keepAlive) {
socket.setKeepAlive(true, 0);
}
else {
socket.setEncoding('utf8');
}
- } |
| Client socket | |
| If host is undefined
-Get it from headers | if (!host) {
- host = headers.Host;
}
- |
| Remote host address | var remote_host = server + (port - 80 === 0 ? '' : ':' + port); |
| Change headers | headers.Host = remote_host;
- headers.Origin = 'http://' + remote_host; |
| Open request | var p = manager.getPool(port, server);
-
- p.getClient(function(client) { |
| Based on 'pool/main.js' | var request = client.request('GET', req.url, headers);
-
- var errorListener = function (error) {
- client.removeListener('error', errorListener);
- |
| Remove the client from the pool's available clients since it has errored | p.clients.splice(p.clients.indexOf(client), 1);
- socket.end();
- } |
| Not disconnect on update | client.on('upgrade', function(request, remote_socket, head) { |
| Prepare socket | _socket(remote_socket, true); |
| Emit event | onUpgrade(remote_socket);
- });
-
- client.on('error', errorListener);
- request.on('response', function (response) {
- response.on('end', function () {
- client.removeListener('error', errorListener);
- client.busy = false;
- p.onFree(client);
- })
- })
- client.busy = true;
-
- var handshake;
- 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(remote_host, host)
- .replace(remote_host, host);
-
- try { |
| Write printable | |
| 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(e) {
- request.end();
+
+ function onUpgrade(out, reverseProxy) {
+ if (!out) {
+ reverseProxy.end();
socket.end();
+ return;
}
- self.unwatch(socket);
- }); |
| Request | function onUpgrade(reverse_proxy) {
+
var listeners = {};
- |
| We're now connected to the server, so lets change server socket | reverse_proxy.on('data', listeners._r_data = function(data) { |
| Pass data to client | |
| 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 (out.incoming.socket.writable) {
try {
- socket.write(data);
+ out.incoming.socket.write(data);
}
catch (e) {
- socket.end();
- reverse_proxy.end();
+ out.incoming.socket.end();
+ reverseProxy.end();
}
}
});
- socket.on('data', listeners._data = function(data) { |
| Pass data from client to server | try {
- reverse_proxy.write(data);
+ out.incoming.socket.on('data', listeners._data = function(data) { |
| Pass data from client to server | try {
+ reverseProxy.write(data);
}
catch (e) {
- reverse_proxy.end();
+ reverseProxy.end();
socket.end();
}
- }); |
| Detach event listeners from reverse_proxy | function detach() {
- reverse_proxy.removeListener('close', listeners._r_close);
- reverse_proxy.removeListener('data', listeners._r_data);
- socket.removeListener('data', listeners._data);
- socket.removeListener('close', listeners._close);
- } |
| Hook disconnections | reverse_proxy.on('end', listeners._r_close = function() {
- socket.end();
+ }); |
| Detach event listeners from reverseProxy | function detach() {
+ reverseProxy.removeListener('close', listeners._r_close);
+ reverseProxy.removeListener('data', listeners._r_data);
+ out.incoming.socket.removeListener('data', listeners._data);
+ out.incoming.socket.removeListener('close', listeners._close);
+ } |
| Hook disconnections | reverseProxy.on('end', listeners._r_close = function() {
+ out.incoming.socket.end();
detach();
});
socket.on('end', listeners._close = function() {
- reverse_proxy.end();
+ reverseProxy.end();
detach();
});
+ }; |
| Client 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,
+ method: 'GET',
+ path: req.url,
+ headers: req.headers,
+ }; |
| Make the outgoing WebSocket request | var request = agent.appendMessage(outgoing); |
| Here we set the incoming req, socket and head data to the outgoing
+request so that we can reuse this data later on in the closure scope
+available to the upgrade event. This bookkeeping is not tracked anywhere
+in nodejs core and is very specific to proxying WebSockets. | request.agent = agent;
+ request.incoming = {
+ request: req,
+ socket: socket,
+ head: head
};
+ |
| If the agent for this particular host and port combination
+is not already listening for the upgrade event, then do so once.
+This will force us not to disconnect.
+
+In addition, it's important to note the closure scope here. Since
+there is no mapping of the | if (!agent._events || agent._events['upgrade'].length === 0) {
+ agent.on('upgrade', function (out, remoteSocket, head) { |
| Prepare socket | _socket(remoteSocket, true);
+ |
| Emit event | onUpgrade(remoteSocket._httpMessage, remoteSocket);
+ });
+ }
+
+ if (typeof request.socket !== 'undefined') {
+ request.socket.on('data', function handshake (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 | |
| 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();
+ }
};
|