Commits

askurihin  committed e4c7c56

Initial commit

  • Participants

Comments (0)

Files changed (6)

+language: node_js
+node_js:
+  - 0.6
+  - 0.8
+  - 0.9
+
+notifications:
+  email:
+    - travis@nodejitsu.com
+# Patron
+
+Wrapper around node-http-proxy with plugins, logging, and simple
+configuration format.
+var bunyan = require('bunyan')
+  , fs = require('fs')
+  , crypto = require('crypto')
+  , S = require('string')
+
+var logfile = fs.createWriteStream(process.argv[2] || __dirname + '/proxy.log');
+
+var logger = bunyan.createLogger({
+  name: 'proxy',
+  stream: logfile
+});
+
+var logreq = function(req) {
+  return {
+    headers: req.headers,
+    method: req.method,
+    ip: req.connection.remoteAddress,
+    url: req.url
+  };
+};
+
+module.exports = function(req) {
+  var req_id = crypto.randomBytes(4).toString('hex');
+  req.logger = logger.child({
+    serializers: bunyan.stdSerializers,
+    req_id: req_id
+  });
+  req.log = function(msg) {
+    return req.logger.info(logreq(req), msg);
+  };
+  req.headers['X-Request-Id'] = req_id;
+  // add usefull string methods to req.url
+  req.url = S(req.url)
+  return req;
+};
+module.exports = function(proxyTable) {
+
+var http = require('http')
+  , url = require('url')
+  , httpProxy = require('http-proxy')
+  , debug = require('debug')('lib-proxy')
+  , S = require('string')
+  , EE = require('events').EventEmitter
+
+var decorate = require('./decorate.js')
+var server = http.createServer();
+var bus = new EE();
+
+bus.on('add', function(rule) {
+  var ruleName = getFirstKey(rule)
+  debug('Got add event')
+  proxyTable[ruleName] = rule[ruleName]
+  debug('rule for %s was added', ruleName)
+})
+
+bus.on('remove', function(ruleName) {
+  debug('Got remove event')
+  delete proxyTable[ruleName]
+  debug('rule for %s was removed', ruleName)
+})
+
+server.use = function use(plugin) {
+  plugin(bus)
+}
+
+function getFirstKey(obj) { return Object.keys(obj)[0] }
+
+server.on('request', function(req, res) {
+  debug('new HTTP request')
+  handleProxying({
+    req: decorate(req),
+    res: res,
+    protocol: 'http'
+  })
+
+})
+server.on('upgrade', function(req, socket, head) {
+  debug('new UPGRADE request')
+  handleProxying({
+    req: decorate(req),
+    socket: socket,
+    head: head,
+    protocol: 'websockets'
+  })
+})
+
+function handleProxying(net) {
+  var href = net.req.headers.host + net.req.url
+  // get proxy rule for this host + url
+  var rule = proxyTableLookup(proxyTable, href)
+  if (!rule) {
+    net.req.logger.error('Bad request');
+    net.res.writeHead(404, {
+      'Content-Type': 'text/plain'
+    });
+    net.res.write('You are wrong');
+    return net.res.end()
+  }
+  // rule is either function, string or object
+  if (typeof rule === 'function') {
+    debug('Found function rule')
+    rule(net, proxy)
+  } else if (typeof rule === 'string'
+             // by default don't proxy websockets
+             && net.protocol === 'http') {
+    debug('Found string rule %s', rule)
+    proxy(net, rule)
+  } else {
+    if ( rule.ws === true && net.protocol === 'websockets') {
+      proxy(net, rule.to)
+    } else if ( rule.http !== false && net.prototype === 'http' ) {
+      proxy(net, rule.to)
+    }
+  }
+}
+
+// find rule for href in proxyTable
+function proxyTableLookup(proxyTable, href) {
+  debug('Doing lookup in proxyTable for %s', href)
+  for (var key in proxyTable) {
+    if (S(href).startsWith(key)) {
+      return proxyTable[key]
+    }
+  }
+}
+
+// do actual proxying
+function proxy(net, address) {
+  debug('Proxying %s%s to %s%s', net.req.headers.host, net.req.url, address, net.req.url)
+  debug('Protocol is %s', net.protocol)
+  if (net.protocol === 'http') {
+    net.req.logger.info('Proxying http to ' + address)
+    return proxyFor(address).proxyRequest(net.req, net.res)
+  } else if (net.protocol === 'websockets') {
+    net.req.logger.info('Proxying websockets to ' + address)
+    return proxyFor(address).proxyWebSocketRequest(net.req, net.socket, net.head)
+  }
+}
+
+
+// create or return already created proxy for address
+var proxyies = {}
+function proxyFor(address) {
+  if (!S(address).startsWith('http://')) {
+    address = 'http://' + address
+  }
+  var host = url.parse(address).hostname
+  var port = url.parse(address).port
+  debug('Host %s, port %s', host, port)
+  if (!proxyies[address]) {
+    proxyies[host] = new httpProxy.HttpProxy({
+      target: {
+        host: host,
+        port: port
+      }
+    })
+  }
+  return proxyies[host]
+}
+
+
+return server
+}

File package.json

+{
+  "name": "patron",
+  "version": "0.0.1",
+  "description": "node-http-proxy wrapper",
+  "keywords": ["proxy", "reverse", "http"],
+  "author": "ask <a.skurihin@gmail.com>",
+  "dependencies": {
+      "http-proxy": "*",
+      "bunyan": "*",
+      "string": "*",
+      "debug": "*"
+  },
+  "devDependencies": {
+      "tap": "*",
+      "request": "*"
+  },
+  "scripts": {
+      "test": "npm test"
+  },
+  "main": "index"
+}

File test/index.js

+var test = require('tap').test
+var createProxy = require('index.js')
+var http = require('http')
+var request = require('request')
+
+
+var router = {
+    // url based proxying support [done]
+    'example.com/con': 'localhost:3500',
+    // by default proxy only http [done]
+    'example.com/bar': 'localhost:4000',
+    // order is respected, thas't important
+    // if define just domain first, it will ignore all next defined domain/url
+    // [done]
+    'example.com': 'localhost:6780',
+    // proxy both http and websocket
+    'example.net': {to: 'localhost:5000', ws:true},
+    // proxy only websockets
+    'blabla.com': {to: 'localhost:6000', ws:true, http: false},
+    // custom logic support [done]
+    'another.net': function(net, proxy) {
+      if (net.req.url == '/bla') {
+        // proxy based on url
+        return proxy(net, 'localhost:6778')
+      }
+      else if (net.req.url == '/rewrite') {
+        // url rewrite
+        net.req.url = '/rewrite/url'
+        // websocket support
+        return proxy(net, 'localhost:6712')
+      }
+    },
+    'anotherone.org': function(net, proxy) {
+      // inline responce [done]
+      return net.res.end('Error')
+    }
+}
+
+// create test proxy
+var proxy = createProxy(router);
+proxy.listen(3000)
+
+// create test server on given port
+function createServer (port) {
+  return http.createServer().listen(port)
+}
+
+test('Testing host + url', function(t) {
+  var s = createServer(3500)
+  s.on('request', function(req, res) {
+    t.equal(req.headers.host, 'example.com', 'Host should be the same, that in original request')
+    t.equal(req.url, '/con', 'Url should be the same, that in original request')
+    res.end()
+    s.close()
+    t.end()
+  })
+  request({
+    url:'http://localhost:3000/con',
+    headers: {'host': 'example.com'}
+  })
+})
+test('Testing same host + different url', function(t) {
+  var s = createServer(4000)
+  s.on('request', function(req, res) {
+    t.equal(req.headers.host, 'example.com', 'Host should be the same, that in original request')
+    t.equal(req.url, '/bar', 'Url should be the same, that in original request')
+    res.end()
+    s.close()
+    t.end()
+  })
+  request({
+    url:'http://localhost:3000/bar',
+    headers: {'host': 'example.com'}
+  })
+})
+test('Testing same host without url', function(t) {
+  var s = createServer(6780)
+  s.on('request', function(req, res) {
+    t.equal(req.headers.host, 'example.com', 'Host should be the same, that in original request')
+    t.equal(req.url, '/', 'Url should be empty')
+    res.end()
+    s.close()
+    t.end()
+  })
+  request({
+    url:'http://localhost:3000',
+    headers: {'host': 'example.com'}
+  })
+})
+test('Proxying based on custom logic with url when rule is function', function(t) {
+  var s = createServer(6778)
+  s.on('request', function(req, res) {
+    t.equal(req.headers.host, 'another.net', 'Host should be the same, that in original request')
+    t.equal(req.url, '/bla', 'Url should be the same, that in original request')
+    res.end()
+    s.close()
+    t.end()
+  })
+  request({
+    url:'http://localhost:3000/bla',
+    headers: {'host': 'another.net'}
+  })
+})
+test('Proxying with url rewrite when rule is a function', function(t) {
+  var s = createServer(6712)
+  s.on('request', function(req, res) {
+    t.equal(req.headers.host, 'another.net', 'Host should be the same, that in original request')
+    t.equal(req.url, '/rewrite/url', 'Url should be rewriten')
+    res.end()
+    s.close()
+    t.end()
+    //proxy.close()
+  })
+  request({
+    url:'http://localhost:3000/rewrite',
+    headers: {'host': 'another.net'}
+  })
+})
+
+test('Testing inline responce', function(t) {
+  request({
+    url:'http://localhost:3000',
+    headers: {'host': 'anotherone.org'}
+  },function(err, res, body) {
+    t.equal(body, 'Error', 'Responce body should be the same as in the rule')
+    t.end()
+    proxy.close()
+  })
+})