2 node-http-proxy.js: Lookup table for proxy targets in node.js
4 Copyright (c) 2010 Charlie Robbins
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:
14 The above copyright notice and this permission notice shall be
15 included in all copies or substantial portions of the Software.
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.
27 var util = require('util'),
28 events = require('events'),
33 // ### function ProxyTable (router, silent)
34 // #### @router {Object} Object containing the host based routes
35 // #### @silent {Boolean} Value indicating whether we should suppress logs
36 // #### @hostnameOnly {Boolean} Value indicating if we should route based on __hostname string only__
37 // #### @pathnameOnly {Boolean} Value indicating if we should route based on only the pathname. __This causes hostnames to be ignored.__. Using this along with hostnameOnly wont work at all.
38 // Constructor function for the ProxyTable responsible for getting
39 // locations of proxy targets based on ServerRequest headers; specifically
40 // the HTTP host header.
42 var ProxyTable = exports.ProxyTable = function (options) {
43 events.EventEmitter.call(this);
45 this.silent = options.silent || options.silent !== true;
46 this.target = options.target || {};
47 this.pathnameOnly = options.pathnameOnly === true;
48 this.hostnameOnly = options.hostnameOnly === true;
50 if (typeof options.router === 'object') {
52 // If we are passed an object literal setup
53 // the routes with RegExps from the router
55 this.setRoutes(options.router);
57 else if (typeof options.router === 'string') {
59 // If we are passed a string then assume it is a
60 // file path, parse that file and watch it for changes
63 this.routeFile = options.router;
64 this.setRoutes(JSON.parse(fs.readFileSync(options.router)).router);
66 fs.watchFile(this.routeFile, function () {
67 fs.readFile(self.routeFile, function (err, data) {
69 self.emit('error', err);
72 self.setRoutes(JSON.parse(data).router);
73 self.emit('routes', self.hostnameOnly === false ? self.routes : self.router);
78 throw new Error('Cannot parse router with unknown type: ' + typeof router);
83 // Inherit from `events.EventEmitter`
85 util.inherits(ProxyTable, events.EventEmitter);
88 // ### function addRoute (route, target)
89 // #### @route {String} String containing route coming in
90 // #### @target {String} String containing the target
91 // Adds a host-based route to this instance.
93 ProxyTable.prototype.addRoute = function (route, target) {
95 throw new Error('Cannot update ProxyTable routes without router.');
98 this.router[route] = target;
99 this.setRoutes(this.router);
103 // ### function removeRoute (route)
104 // #### @route {String} String containing route to remove
105 // Removes a host-based route from this instance.
107 ProxyTable.prototype.removeRoute = function (route) {
109 throw new Error('Cannot update ProxyTable routes without router.');
112 delete this.router[route];
113 this.setRoutes(this.router);
117 // ### function setRoutes (router)
118 // #### @router {Object} Object containing the host based routes
119 // Sets the host-based routes to be used by this instance.
121 ProxyTable.prototype.setRoutes = function (router) {
123 throw new Error('Cannot update ProxyTable routes without router.');
127 this.router = router;
129 if (this.hostnameOnly === false) {
132 Object.keys(router).forEach(function (path) {
133 if (!/http[s]?/.test(router[path])) {
134 router[path] = (self.target.https ? 'https://' : 'http://')
138 var target = url.parse(router[path]),
139 defaultPort = self.target.https ? 443 : 80;
142 // Setup a robust lookup table for the route:
146 // regexp: /^foo.com/i,
149 // protocol: 'http:',
152 // hostname: 'foo.com',
153 // href: 'http://foo.com/',
160 // sref: '127.0.0.1:8000/',
162 // protocol: 'http:',
164 // host: '127.0.0.1:8000',
165 // hostname: '127.0.0.1',
166 // href: 'http://127.0.0.1:8000/',
174 regexp: new RegExp('^' + path, 'i'),
176 url: url.parse('http://' + path)
179 sref: target.hostname + ':' + (target.port || defaultPort) + target.path,
188 // ### function getProxyLocation (req)
189 // #### @req {ServerRequest} The incoming server request to get proxy information about.
190 // Returns the proxy location based on the HTTP Headers in the ServerRequest `req`
191 // available to this instance.
193 ProxyTable.prototype.getProxyLocation = function (req) {
194 if (!req || !req.headers || !req.headers.host) {
198 var targetHost = req.headers.host.split(':')[0];
199 if (this.hostnameOnly === true) {
200 var target = targetHost;
201 if (this.router.hasOwnProperty(target)) {
202 var location = this.router[target].split(':'),
204 port = location.length === 1 ? 80 : location[1];
212 else if (this.pathnameOnly === true) {
213 var target = req.url;
214 for (var i in this.routes) {
215 var route = this.routes[i];
217 // If we are matching pathname only, we remove the matched pattern.
219 // IE /wiki/heartbeat
223 // for the route "/wiki" : "127.0.0.1:8020"
225 if (target.match(route.source.regexp)) {
226 req.url = url.format(target.replace(route.source.regexp, ''));
228 protocol: route.target.url.protocol.replace(':', ''),
229 host: route.target.url.hostname,
230 port: route.target.url.port
231 || (this.target.https ? 443 : 80)
238 var target = targetHost + req.url;
239 for (var i in this.routes) {
240 var route = this.routes[i];
241 if (target.match(route.source.regexp)) {
243 // Attempt to perform any path replacement for differences
244 // between the source path and the target path. This replaces the
245 // path's part of the URL to the target's part of the URL.
247 // 1. Parse the request URL
248 // 2. Replace any portions of the source path with the target path
249 // 3. Set the request URL to the formatted URL with replacements.
251 var parsed = url.parse(req.url);
253 parsed.pathname = parsed.pathname.replace(
254 route.source.url.pathname,
255 route.target.url.pathname
258 req.url = url.format(parsed);
261 protocol: route.target.url.protocol.replace(':', ''),
262 host: route.target.url.hostname,
263 port: route.target.url.port
264 || (this.target.https ? 443 : 80)
274 // ### close function ()
275 // Cleans up the event listeneners maintained
278 ProxyTable.prototype.close = function () {
279 if (typeof this.routeFile === 'string') {
280 fs.unwatchFile(this.routeFile);