From b29a754fcf661402811678db6d76f87a496416a4 Mon Sep 17 00:00:00 2001 From: Tomas Aparicio Date: Sun, 21 Jun 2015 13:02:37 +0100 Subject: [PATCH] feat(#1,#2): support features. refactors --- .codeclimate.yml | 4 ++ README.md | 12 ++-- lib/rocky.js | 40 ++++--------- lib/route.js | 25 ++++---- lib/server.js | 34 +++++++++++ package.json | 4 +- test/rocky.js | 151 +++++++++++++++++++++++++++++++++++++++++++---- 7 files changed, 214 insertions(+), 56 deletions(-) create mode 100644 .codeclimate.yml create mode 100644 lib/server.js diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..c0e3cee --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,4 @@ +languages: + JavaScript: true +exclude_paths: + - "examples/*.js" diff --git a/README.md b/README.md index 55648bf..20ef11c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# rocky +# rocky [![Build Status](https://api.travis-ci.org/h2non/rocky.svg?branch=master&style=flat)][travis] [![Code Climate](https://codeclimate.com/github/h2non/rocky/badges/gpa.svg)](https://codeclimate.com/github/h2non/rocky) [![NPM](https://img.shields.io/npm/v/rocky.svg)](https://www.npmjs.org/package/rocky) ![Downloads](https://img.shields.io/npm/dm/rocky.svg) -Extensible and plugable HTTP migration library for [node.js](http://nodejs.org). + + +Plugable HTTP proxy and migration library for [node.js](http://nodejs.org). `rocky` essentially acts as a reverse HTTP proxy forwaring the traffic to one or multiple backends with optional traffic replay. @@ -10,11 +12,13 @@ It provides an elegant and fluent programmatic API with built-in features such a ## Features -- Full featured HTTP/S proxy (backed by [http-proxy](https://github.com/nodejitsu/node-http-proxy)) +- Full-featured HTTP/S proxy (backed by [http-proxy](https://github.com/nodejitsu/node-http-proxy)) - Replay traffic to multiple backends - Works as standalone HTTP/S server - Or integrated with connect/express via middleware -- Full-featured path based routing (express based) +- Path based forwarding and replay +- Full-featured router (express-like) +- Routing support based on regexp and wildcards - Built-in middleware layer - HTTP traffic interceptors via events - Fluent and elegant API diff --git a/lib/rocky.js b/lib/rocky.js index cc92b4d..851e7b0 100644 --- a/lib/rocky.js +++ b/lib/rocky.js @@ -1,9 +1,8 @@ -var parseUrl = require('url').parse -var router = require('router') -var http = require('http') -var https = require('https') -var httpProxy = require('http-proxy') -var Route = require('./route') +var router = require('router') +var httpProxy = require('http-proxy') +var parseUrl = require('url').parse +var Route = require('./route') +var createServer = require('./server') module.exports = Rocky @@ -53,30 +52,17 @@ Rocky.prototype.middleware = function () { } Rocky.prototype.listen = function (port, host) { - var handler = serverHandler.bind(this) - - this.server = this.opts.ssl ? - https.createServer(this.opts.ssl, handler) : - http.createServer(handler) - - this.server.listen(port, host) - + var opts = { ssl: this.opts.ssl, port: port, host: host } + var handler = this.requestHandler.bind(this) + this.server = createServer(opts, handler) return this } -function serverHandler(req, res) { - this.requestHandler(req, res, function (err) { - if (err) { - return reply(res, 500, 'Server error: ' + err) - } - reply(res, 404, 'No route configured') - }) -} - -function reply(res, code, body) { - res.writeHead(code, { 'Content-Type': 'application/json' }) - res.write(JSON.stringify({ message: body })) - res.end() +Rocky.prototype.close = function (cb) { + if (this.server) { + this.server.close(cb) + } + return this } ;['get', 'post', 'delete', 'patch', 'put', 'options', 'all'].forEach(function (method) { diff --git a/lib/route.js b/lib/route.js index 79ad2c6..68f0417 100644 --- a/lib/route.js +++ b/lib/route.js @@ -1,12 +1,12 @@ -var http = require('http') -var _ = require('lodash') +var _ = require('lodash') +var http = require('http') var Emitter = require('events').EventEmitter module.exports = Route function Route(path) { - this.target = null this.path = path + this.target = null this.replays = [] Emitter.call(this) } @@ -29,12 +29,13 @@ Route.prototype._handle = function (rocky) { return function handler(req, res) { var target = route.target || rocky.opts.target.href var opts = _.assign({}, rocky.opts, { target: target }) + var eventProxy = proxyHandler(route, res, res) route.emit('start', opts, req, res) // Forward the request to the main target if (target) { - rocky.proxy.web(req, res, opts, proxyHandler('forward')) + rocky.proxy.web(req, res, opts, eventProxy('forward')) } // Replay the request if necessary @@ -46,16 +47,18 @@ Route.prototype._handle = function (rocky) { replays.forEach(function (url) { var res = new http.ServerResponse(req) var opts = _.assign({}, rocky.opts, { target: url }) - rocky.proxy.web(req, res, opts, proxyHandler('replay')) + rocky.proxy.web(req, res, opts, eventProxy('replay')) }) + } +} - function proxyHandler(type) { - return function (err) { - if (err) { - return route.emit(type + ':error', err, req, res) - } - route.emit(type + ':success', req, res) +function proxyHandler(route, req, res) { + return function (type) { + return function (err) { + if (err) { + return route.emit(type + ':error', err, req, res) } + route.emit(type + ':success', req, res) } } } diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..7630ce5 --- /dev/null +++ b/lib/server.js @@ -0,0 +1,34 @@ +var http = require('http') +var https = require('https') +var version = require('../package.json').version + +module.exports = createServer + +function createServer(opts, router) { + var handler = serverHandler(router) + + var server = opts.ssl + ? https.createServer(opts.ssl, handler) + : http.createServer(handler) + + server.listen(opts.port, opts.host) + return server +} + +function serverHandler(router) { + return function (req, res) { + res.setHeader('Server', 'rocky ' + version) + router(req, res, function (err) { + if (err) { + return reply(res, 500, err) + } + reply(res, 404, 'No route configured') + }) + } +} + +function reply(res, code, body) { + res.writeHead(code, { 'Content-Type': 'application/json' }) + res.write(JSON.stringify({ message: body })) + res.end() +} diff --git a/package.json b/package.json index 565363f..6cfe0db 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Tomas Aparicio", "license": "MIT", "scripts": { - "test": "./node_modules/.bin/mocha --timeout 5000 --reporter spec --ui tdd" + "test": "./node_modules/.bin/mocha --timeout 2000 --reporter spec --ui tdd" }, "keywords": [ "http", @@ -31,6 +31,6 @@ "chai": "^3.0.0", "connect": "^3.4.0", "mocha": "^2.2.5", - "nock": "^2.6.0" + "supertest": "^1.0.1" } } diff --git a/test/rocky.js b/test/rocky.js index 45481b2..5793857 100644 --- a/test/rocky.js +++ b/test/rocky.js @@ -1,44 +1,171 @@ const http = require('http') +const supertest = require('supertest') const expect = require('chai').expect const rocky = require('../') -const ports = { target: 9890, proxy: 9891 } +const ports = { target: 9890, proxy: 9891, replay: 9892 } const baseUrl = 'http://127.0.0.1' +const proxyUrl = baseUrl + ':' + ports.proxy const targetUrl = baseUrl + ':' + ports.target +const replayUrl = baseUrl + ':' + ports.replay const noop = function () {} suite('rocky', function () { - var proxy = null + var proxy, replay, server - afterEach(function () { - proxy.server.close() + beforeEach(function () { + proxy = replay = server = null + }) + + afterEach(function (done) { + if (replay) replay.close() + if (server) server.close() + if (proxy) proxy.server.close() + setTimeout(done, 10) }) test('simple forward', function (done) { proxy = rocky() - .forward('http://127.0.0.1:' + ports.target) + .forward(targetUrl) + .listen(ports.proxy) + + server = createTestServer(assert) + + proxy.get('/test') + http.get(proxyUrl + '/test', noop) + + function assert(req, res) { + expect(req.url).to.be.equal('/test') + expect(res.statusCode).to.be.equal(200) + done() + } + }) + + test('forward and replay', function (done) { + proxy = rocky() + .forward(targetUrl) + .replay(replayUrl) .listen(ports.proxy) proxy.get('/test') - createTestServer(assert) + replay = createReplayServer(assertReplay) + server = createTestServer(assert) - http.get(targetUrl + '/test', noop) + supertest(proxyUrl) + .get('/test') + .expect(200) + .expect('Content-Type', 'application/json') + .expect({ 'hello': 'world' }) + .end(done) function assert(req, res) { expect(req.url).to.be.equal('/test') expect(res.statusCode).to.be.equal(200) - done() + } + + function assertReplay(req, res) { + expect(req.url).to.be.equal('/test') + expect(res.statusCode).to.be.equal(204) + } + }) + + test('forward and replay to multiple backends', function (done) { + proxy = rocky() + .forward(targetUrl) + .replay(replayUrl) + .replay(replayUrl) + .replay(replayUrl) + .listen(ports.proxy) + + proxy.get('/test') + + replay = createReplayServer(assertReplay) + server = createTestServer(assert) + + supertest(proxyUrl) + .get('/test') + .expect(200) + .expect('Content-Type', 'application/json') + .expect({ 'hello': 'world' }) + .end(noop) + + function assert(req, res) { + expect(req.url).to.be.equal('/test') + expect(res.statusCode).to.be.equal(200) + } + + var asserts = 0 + function assertReplay(req, res) { + expect(req.url).to.be.equal('/test') + expect(res.statusCode).to.be.equal(204) + + asserts += 1 + if (asserts === 3) { + done() + } + } + }) + + test('forward and replay payload data', function (done) { + proxy = rocky() + .forward(targetUrl) + .replay(replayUrl) + .listen(ports.proxy) + + proxy.post('/test') + + replay = createReplayServer(assertReplay) + server = createTestServer(assert) + + supertest(proxyUrl) + .post('/test') + .send({ hello: 'world' }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect({ 'hello': 'world' }) + .end(done) + + function assert(req, res) { + expect(req.url).to.be.equal('/test') + expect(res.statusCode).to.be.equal(200) + expect(req.body).to.match(/hello/) + } + + var asserts = 0 + function assertReplay(req, res) { + expect(req.url).to.be.equal('/test') + expect(res.statusCode).to.be.equal(204) + expect(req.body).to.match(/hello/) } }) }) function createTestServer(assert) { + return createServer(ports.target, 200, assert) +} + +function createReplayServer(assert) { + return createServer(ports.replay, 204, assert) +} + +function createServer(port, code, assert) { var server = http.createServer(function (req, res) { - res.writeHead(200) - res.end() - assert(req, res) + res.writeHead(code, { 'Content-Type': 'application/json' }) + res.write(JSON.stringify({ 'hello': 'world' })) + + var body = '' + req.on('data', function (data) { + body += data + }) + req.on('end', function () { + req.body = body + assert(req, res) + res.end() + }) }) - server.listen(ports.target) + + server.listen(port) + return server }