toxy
Advanced tools
Comparing version 0.2.2 to 0.3.0
@@ -26,3 +26,3 @@ const toxy = require('..') | ||
.get('/image/*') | ||
.poison(poisons.bandwidth(1024)) | ||
.outgoingPoison(poisons.bandwidth(1024)) | ||
@@ -38,3 +38,3 @@ proxy | ||
// Enable the admin HTTP server | ||
var admin = toxy.admin(/* { apiKey: 's3cr3t' }*/) | ||
var admin = toxy.admin(/* { apiKey: 's3cr3t' } */) | ||
@@ -41,0 +41,0 @@ // Admin server is middleware-oriented, too :) |
@@ -11,2 +11,4 @@ const toxy = require('..') | ||
proxy | ||
.rule(rules.method('GET')) | ||
.poison(poisons.inject({ | ||
@@ -18,4 +20,8 @@ code: 503, | ||
.withRule(rules.probability(50)) | ||
.rule(rules.method('GET')) | ||
.outgoingPoison(poisons.bandwidth({ | ||
bytes: 1024 | ||
})) | ||
.withRule(rules.probability(50)) | ||
// Get poison | ||
@@ -25,2 +31,6 @@ var poison = proxy.getPoison('inject') | ||
// Get outgoing poison | ||
var poison = proxy.getOutgoingPoison('bandwidth') | ||
poison.isEnabled() // -> true | ||
// Get rule in the poison scope (nested) | ||
@@ -47,2 +57,5 @@ var rule = poison.getRule('probability') // -> Directive | ||
// Outgoing poison | ||
proxy.isEnabledOutgoing('bandwidth') // -> true | ||
// Flush all poisons (not necessary, though) | ||
@@ -49,0 +62,0 @@ proxy.flush() |
@@ -22,2 +22,3 @@ const Toxy = require('./lib/toxy') | ||
toxy.Base = require('./lib/base') | ||
toxy.Poison = require('./lib/poison') | ||
toxy.Directive = require('./lib/directive') | ||
@@ -24,0 +25,0 @@ toxy.Rocky = require('rocky').Rocky |
const router = require('router') | ||
const createServer = require('./server') | ||
const randomId = require('../common').randomId | ||
const randomId = require('../helpers').randomId | ||
@@ -5,0 +5,0 @@ module.exports = Admin |
@@ -1,3 +0,7 @@ | ||
const parentHref = require('./common').parentHref | ||
const parentHref = require('./helpers').parentHref | ||
exports.createPoison = createPartial('poisons') | ||
exports.createRule = createPartial('rules') | ||
exports.allPoisons = function (req, res) { | ||
@@ -11,46 +15,8 @@ res.reply(exports.poisons(req)) | ||
exports.createPoison = createPartial('poisons') | ||
exports.createRule = createPartial('rules') | ||
function createPartial(type) { | ||
return function (req, res) { | ||
var body = createDirective(req, res, type) | ||
if (body) res.reply(body, 201) | ||
} | ||
exports.getPoison = function (req, res) { | ||
res.reply(getDirective(req, 'poisons')(req.toxyPoison)) | ||
} | ||
function createDirective(req, res, type) { | ||
var body = req.body | ||
var toxy = getStack(req) | ||
var directives = req.toxy[type] | ||
if (!body || !body.name) { | ||
res.statusCode = 400 | ||
res.end() | ||
return | ||
} | ||
var name = body.name | ||
var directive = directives[name] | ||
if (typeof directive !== 'function') { | ||
res.statusCode = 404 | ||
res.end() | ||
return | ||
} | ||
var method = type === 'poisons' ? 'poison' : 'rule' | ||
toxy[method](directive(body.options)) | ||
var href = req.href + '/' + type + '/' + name | ||
var links = { | ||
self: { href: href }, | ||
parent: { href: parentHref(href) } | ||
} | ||
return { | ||
name: name, | ||
links: links | ||
} | ||
exports.getRule = function (req, res) { | ||
res.reply(getDirective(req, 'rules')(req.toxyRule)) | ||
} | ||
@@ -70,15 +36,2 @@ | ||
function deleteAll(req, type) { | ||
var stack = getStack(req)['_' + type] | ||
if (stack) stack.stack.splice(0) | ||
} | ||
exports.getPoison = function (req, res) { | ||
res.reply(getDirective(req, 'poisons')(req.toxyPoison)) | ||
} | ||
exports.getRule = function (req, res) { | ||
res.reply(getDirective(req, 'rules')(req.toxyRule)) | ||
} | ||
exports.deletePoison = function (req, res) { | ||
@@ -116,5 +69,5 @@ var stack = req.toxyRoute || req.toxy | ||
function getDirective(req, type) { | ||
function getDirective(req, type, addPhase) { | ||
return function (directive) { | ||
var nested = !~req.href.indexOf(type) | ||
var href = !~req.href.indexOf(type) | ||
? req.href + '/' + type + '/' + directive.name | ||
@@ -129,9 +82,18 @@ : req.href | ||
if (type === 'poisons') { | ||
// Expose poison phase | ||
data.phase = directive.phase | ||
// Add phase to href | ||
href += !~req.href.indexOf(':') | ||
? ':' + directive.phase | ||
: '' | ||
// Retrieve poisons specific rules | ||
var rules = directive.getRules() | ||
data.rules = rules.map(getDirective({ href: nested }, 'rules')) | ||
data.rules = rules.map(getDirective({ href: href }, 'rules')) | ||
} | ||
data.links = { | ||
self: { href: nested }, | ||
parent: { href: parentHref(nested) } | ||
self: { href: href }, | ||
parent: { href: parentHref(href) } | ||
} | ||
@@ -143,4 +105,63 @@ | ||
function createPartial(type) { | ||
return function (req, res) { | ||
var body = createDirective(req, res, type) | ||
if (body) res.reply(body, 201) | ||
} | ||
} | ||
function createDirective(req, res, type) { | ||
var body = req.body | ||
var toxy = getStack(req) | ||
var directives = req.toxy[type] | ||
if (!body || !body.name) { | ||
res.statusCode = 400 | ||
res.end() | ||
return | ||
} | ||
var name = body.name | ||
var directive = directives[name] | ||
if (typeof directive !== 'function') { | ||
res.statusCode = 404 | ||
res.end() | ||
return | ||
} | ||
var method = getCreateMethod(type, body) | ||
var directive = toxy[method](directive(body.options)) | ||
var href = req.href + '/' + type + '/' + name | ||
if (type === 'poisons') { | ||
href += ':' + directive.phase | ||
} | ||
var links = { | ||
self: { href: href }, | ||
parent: { href: parentHref(href) } | ||
} | ||
return { | ||
name: name, | ||
links: links | ||
} | ||
} | ||
function getCreateMethod(type, body) { | ||
if (body.phase === 'outgoing') { | ||
return 'outgoingPoison' | ||
} | ||
return type.slice(0, -1) | ||
} | ||
function deleteAll(req, type) { | ||
var stack = getStack(req)['_' + type] | ||
if (stack) stack.stack.splice(0) | ||
} | ||
function getStack(req) { | ||
return req.toxyRule || req.toxyPoison || req.toxyRoute || req.toxy | ||
} |
@@ -9,2 +9,13 @@ exports.serverParam = function (req, res, next, serverId) { | ||
exports.routeParam = function (req, res, next, routeId) { | ||
var routes = req.toxy.routes | ||
var route = req.toxy.findRoute(routeId) | ||
if (!route) return notFound(res) | ||
req.href += '/routes/' + routeId | ||
req.toxyRoute = route | ||
next() | ||
} | ||
exports.ruleParam = function (req, res, next, ruleId) { | ||
@@ -21,3 +32,8 @@ var toxy = req.toxyPoison || req.toxyRoute || req.toxy | ||
var toxy = req.toxyRoute || req.toxy | ||
req.toxyPoison = toxy.getPoison(poisonId) | ||
var pair = poisonId.split(':') | ||
var name = pair.shift() | ||
var phase = pair.shift() || 'incoming' | ||
req.toxyPoison = toxy.getPoison(name) | ||
if (!req.toxyPoison) return notFound(res) | ||
@@ -29,13 +45,2 @@ | ||
exports.routeParam = function (req, res, next, routeId) { | ||
var routes = req.toxy.routes | ||
var route = req.toxy.findRoute(routeId) | ||
if (!route) return notFound(res) | ||
req.href += '/routes/' + routeId | ||
req.toxyRoute = route | ||
next() | ||
} | ||
function notFound(res) { | ||
@@ -42,0 +47,0 @@ res.writeHead(404, { 'Content-Type': 'application/json'}) |
const directives = require('./directives') | ||
const parentHref = require('./common').parentHref | ||
const parentHref = require('./helpers').parentHref | ||
@@ -4,0 +4,0 @@ exports.all = function (req, res) { |
@@ -38,3 +38,3 @@ module.exports = Base | ||
if (item) { | ||
return item.$of | ||
return item.$of || item | ||
} | ||
@@ -48,3 +48,4 @@ return null | ||
var node = stack[i] | ||
if (node.$name === name || node.$of === name) { | ||
if (node.$name === name || node.$of === name | ||
|| node.name === name || node === name) { | ||
return node | ||
@@ -51,0 +52,0 @@ } |
module.exports = function abort(opts) { | ||
opts = opts || {} | ||
var delay = +opts.delay || 1 | ||
var delay = +opts.delay || 10 | ||
@@ -5,0 +5,0 @@ return function abort(req, res, next) { |
@@ -5,7 +5,7 @@ const throttler = require('./throttle') | ||
if (typeof opts === 'number') { | ||
opts = { bps: opts } | ||
opts = { bytes: opts } | ||
} | ||
opts = opts || {} | ||
opts.bps = +opts.bps || 1024 | ||
opts.bytes = +opts.bps || +opts.bytes || 1024 | ||
opts.threshold = +opts.threshold || 1000 | ||
@@ -12,0 +12,0 @@ |
@@ -0,8 +1,20 @@ | ||
const assign = require('object-assign') | ||
module.exports = function inject(opts) { | ||
opts = opts || {} | ||
var code = +opts.code || 500 | ||
var body = opts.body | ||
var encoding = opts.encoding | ||
return function inject(req, res, next) { | ||
res.writeHead(code, opts.headers) | ||
res.end(opts.body, opts.encoding) | ||
var headers = assign({}, res.headers, opts.headers) | ||
if (body && body.length) { | ||
headers['content-length'] = body.length | ||
} | ||
res.writeHead(code, headers) | ||
res.end(body, encoding) | ||
} | ||
} |
module.exports = function rateLimit(opts) { | ||
opts = opts || {} | ||
var limit = +opts.limit || 10 | ||
@@ -4,0 +5,0 @@ var code = +opts.code || 429 |
@@ -1,2 +0,2 @@ | ||
const common = require('../common') | ||
const helpers = require('../helpers') | ||
@@ -24,3 +24,3 @@ module.exports = function slowRead(opts) { | ||
if (data === null) return writeChunks() | ||
common.splitBuffer(chunkSize, data, encoding, buf) | ||
helpers.splitBuffer(chunkSize, data, encoding, buf) | ||
} | ||
@@ -31,3 +31,3 @@ | ||
ended = true | ||
common.eachSeries(buf, pushDefer, end) | ||
helpers.eachSeries(buf, pushDefer, end) | ||
} | ||
@@ -34,0 +34,0 @@ |
@@ -1,2 +0,2 @@ | ||
const common = require('../common') | ||
const helpers = require('../helpers') | ||
@@ -7,3 +7,3 @@ module.exports = function throttle(opts) { | ||
var threshold = +opts.threshold || 100 | ||
var chunkSize = +opts.bps || +opts.chunk || 1024 | ||
var chunkSize = +opts.bps || +opts.bytes || +opts.chunk || 1024 | ||
@@ -24,3 +24,3 @@ return function throttle(req, res, next) { | ||
res.write = function (data, encoding, done) { | ||
common.splitBuffer(chunkSize, data, encoding, buf) | ||
helpers.splitBuffer(chunkSize, data, encoding, buf) | ||
if (done) done() | ||
@@ -31,7 +31,7 @@ } | ||
if (data && typeof data !== 'function') { | ||
common.splitBuffer(chunkSize, data, encoding, buf) | ||
helpers.splitBuffer(chunkSize, data, encoding, buf) | ||
} | ||
// Party time: write each chunk with a delay in FIFO order | ||
common.eachSeries(buf, writeDefer, end) | ||
helpers.eachSeries(buf, writeDefer, end) | ||
@@ -38,0 +38,0 @@ function end() { |
131
lib/proxy.js
const rocky = require('rocky') | ||
const Rule = require('./rule') | ||
const rules = require('./rules') | ||
const Poison = require('./poison') | ||
const poisons = require('./poisons') | ||
const Directive = require('./directive') | ||
const responseBody = rocky.middleware.responseBody | ||
responseBody.$name = '$outgoingInterceptor$' | ||
module.exports = rocky.Rocky | ||
@@ -14,43 +18,78 @@ | ||
RockyBase.prototype.withRule = | ||
RockyBase.prototype.poisonRule = | ||
RockyBase.prototype.poisonFilter = function (rule) { | ||
if (this.lastPoison) { | ||
this.lastPoison.rule(rule) | ||
} | ||
return this | ||
RockyBase.prototype.enable = function (poison, phase) { | ||
return this._callMethod(this._poisonsStack(phase), 'enable', poison) | ||
} | ||
RockyBase.prototype.enable = function (poison) { | ||
return this._callMethod(this._poisons, 'enable', poison) | ||
RockyBase.prototype.enableOutgoing = function (poison) { | ||
return this.enable(poison, 'outgoing') | ||
} | ||
RockyBase.prototype.disable = function (poison) { | ||
return this._callMethod(this._poisons, 'disable', poison) | ||
RockyBase.prototype.disable = function (poison, phase) { | ||
return this._callMethod(this._poisonsStack(phase), 'disable', poison) | ||
} | ||
RockyBase.prototype.remove = function (poison) { | ||
return this._remove(this._poisons, poison) | ||
RockyBase.prototype.disableOutgoing = function (poison) { | ||
return this.disable(poison, 'outgoing') | ||
} | ||
RockyBase.prototype.isEnabled = function (poison) { | ||
return this._callMethod(this._poisons, 'isEnabled', poison) | ||
RockyBase.prototype.remove = function (poison, phase) { | ||
var stack = this._poisonsStack(phase || 'incoming') | ||
return this._remove(stack, poison) | ||
} | ||
RockyBase.prototype.removeOutgoing = function (poison) { | ||
return this.remove(poison, 'outgoing') | ||
} | ||
RockyBase.prototype.isEnabled = function (poison, phase) { | ||
var stack = this._poisonsStack(phase || 'incoming') | ||
return this._callMethod(stack, 'isEnabled', poison) | ||
} | ||
RockyBase.prototype.isEnabledOutgoing = function (poison) { | ||
return this.isEnabled(poison, 'outgoing') | ||
} | ||
RockyBase.prototype.disableAll = | ||
RockyBase.prototype.disablePoisons = function () { | ||
return this._disableAll(this._poisons) | ||
return this._disableAll(this._poisonsStack()) | ||
} | ||
RockyBase.prototype.getPoison = function (poison) { | ||
return this._getDirective(this._poisons, poison) | ||
RockyBase.prototype.getPoison = function (poison, phase) { | ||
var stack = this._poisonsStack(phase) | ||
var directive = this._getDirective(stack, poison) | ||
if (!directive) return null | ||
if (phase && directive.phase !== phase) return null | ||
return directive | ||
} | ||
RockyBase.prototype.getOutgoingPoison = function (poison) { | ||
return this.getPoison(poison, 'outgoing') | ||
} | ||
RockyBase.prototype.getPoisons = function () { | ||
return this._getAll(this._poisons) | ||
return this._getAll(this._poisonsStack()) | ||
} | ||
RockyBase.prototype.getIncomingPoisons = function () { | ||
return this._getAll(this._inPoisons) | ||
} | ||
RockyBase.prototype.getOutgoingPoisons = function () { | ||
return this._getAll(this._outPoisons) | ||
} | ||
RockyBase.prototype.withRule = | ||
RockyBase.prototype.poisonRule = | ||
RockyBase.prototype.poisonFilter = function (rule) { | ||
if (this.lastPoison) { | ||
this.lastPoison.rule(rule) | ||
} | ||
return this | ||
} | ||
RockyBase.prototype.flush = | ||
RockyBase.prototype.flushPoisons = function () { | ||
this._poisons.stack.splice(0) | ||
this._inPoisons.stack.splice(0) | ||
this._outPoisons.stack.splice(0) | ||
return this | ||
@@ -60,14 +99,58 @@ } | ||
RockyBase.prototype.poison = | ||
RockyBase.prototype.usePoison = function (poison) { | ||
if (!(poison instanceof Directive)) { | ||
poison = new Directive(poison) | ||
RockyBase.prototype.usePoison = | ||
RockyBase.prototype.incomingPoison = function (poison) { | ||
poison = createPoison(poison) | ||
this._inPoisons(poison.handler()) | ||
this.lastPoison = poison | ||
return this | ||
} | ||
RockyBase.prototype.outgoingPoison = | ||
RockyBase.prototype.responsePoison = function (poison) { | ||
poison = createPoison(poison) | ||
if (!this._outPoisonsEnabled) { | ||
this._outPoisonsEnabled = true | ||
this.poison(responseBody(function (req, res, next) { | ||
this._outPoisons.run(req, res, next) | ||
}.bind(this))) | ||
} | ||
this._poisons(poison.handler()) | ||
poison.phase = 'outgoing' | ||
this._outPoisons(poison.handler()) | ||
this.lastPoison = poison | ||
return this | ||
} | ||
RockyBase.prototype._poisonsStack = function (phase) { | ||
if (phase) { | ||
return phase === 'outgoing' | ||
? this._outPoisons | ||
: this._inPoisons | ||
} | ||
var inPoisons = this._inPoisons.stack | ||
var outPoisons = this._outPoisons.stack | ||
var stack = inPoisons.concat(outPoisons) | ||
return { stack: stack } | ||
} | ||
/** | ||
* Extend prototype chain | ||
*/ | ||
Object.keys(Rule.prototype).forEach(function (key) { | ||
RockyBase.prototype[key] = Rule.prototype[key] | ||
}) | ||
/** | ||
* Private helpers | ||
*/ | ||
function createPoison(poison) { | ||
return (poison instanceof Poison) | ||
? poison | ||
: new Poison(poison) | ||
} |
const getRawBody = require('raw-body') | ||
const isRegExp = require('../common').isRegExp | ||
const matchBody = require('../helpers').matchBody | ||
module.exports = function body(opts) { | ||
opts = opts || {} | ||
var match = opts.match | ||
var limit = opts.limit || '5mb' | ||
var encoding = opts.encoding || 'utf8' | ||
@@ -17,12 +20,14 @@ return function body(req, res, next) { | ||
getRawBody(req, { | ||
length: opts.length || req.headers['content-length'], | ||
limit: opts.limit || '5mb', | ||
encoding: opts.encoding || 'utf8' | ||
}, handleBody) | ||
var bodyOpts = { | ||
limit: limit, | ||
encoding: encoding, | ||
length: getLength(req) | ||
} | ||
getRawBody(req, bodyOpts, handleBody) | ||
function handleBody(err, body) { | ||
if (err) return next(err) | ||
// We must expose cached body in the request to forward it properly | ||
// Expose cached body in the request to forward it | ||
req.body = body | ||
@@ -34,22 +39,10 @@ | ||
var notMatches = !matcher(body, next) | ||
next(null, notMatches) | ||
next(null, !matchBody(body, match)) | ||
} | ||
} | ||
function matcher(body, next) { | ||
if (typeof match === 'function') { | ||
return match(body) | ||
} | ||
if (typeof match === 'string') { | ||
return !!~body.indexOf(match) | ||
} | ||
if (isRegExp(match)) { | ||
return match.test(body) | ||
} | ||
return false | ||
function getLength(req) { | ||
return +opts.length | ||
|| +req.headers['content-length'] | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
const isRegExp = require('../common').isRegExp | ||
const match = require('../helpers').matchHeaders | ||
@@ -7,30 +7,4 @@ module.exports = function headers(matchHeaders) { | ||
return function headers(req, res, next) { | ||
next(null, !matches(req, matchHeaders)) | ||
next(null, !match(req, matchHeaders)) | ||
} | ||
} | ||
function matches(req, headers) { | ||
return Object.keys(headers) | ||
.every(function (key) { | ||
var rule = headers[key] | ||
var value = req.headers[key.toLowerCase()] | ||
if (typeof rule === 'boolean') { | ||
return rule ? value != null : value == null | ||
} | ||
if (isRegExp(rule)) { | ||
return rule.test(value) | ||
} | ||
if (typeof rule === 'string') { | ||
return new RegExp(rule, 'i').test(value) | ||
} | ||
if (typeof rule === 'function') { | ||
return rule(value, key) | ||
} | ||
return false | ||
}) | ||
} |
@@ -7,4 +7,7 @@ module.exports = [ | ||
'content-type', | ||
'response-body', | ||
'response-status', | ||
'response-headers' | ||
].map(function (module) { | ||
return require('./' + module) | ||
}) |
const midware = require('midware') | ||
const Proxy = require('./proxy') | ||
const Admin = require('./admin') | ||
const randomId = require('./common').randomId | ||
const randomId = require('./helpers').randomId | ||
@@ -16,5 +16,5 @@ const noop = function () {} | ||
this._rules = midware() | ||
this._poisons = midware() | ||
this._inPoisons = midware() | ||
this._outPoisons = midware() | ||
wrapRouteConstructor(this) | ||
setupMiddleware(this) | ||
@@ -32,41 +32,38 @@ } | ||
Toxy.prototype.findRoute = function (routeId, method) { | ||
if (method) routeId = randomId(method, routeId) | ||
return this.routes.filter(function (route) { | ||
return route.id === routeId | ||
}).filter(function (route) { | ||
return route.unregistered !== true | ||
}).shift() | ||
} | ||
Toxy.prototype.route = function (method, path) { | ||
var route = Proxy.prototype.route.apply(this, arguments) | ||
function wrapRouteConstructor(self) { | ||
var _route = self.route | ||
// Expose toxy route specific data | ||
route.id = randomId(method, path) | ||
route.method = method.toUpperCase() | ||
self.route = function (method, path) { | ||
var route = _route.apply(self, arguments) | ||
// Creates toxy specific route-level middleware | ||
route._rules = midware() | ||
route._inPoisons = midware() | ||
route._outPoisons = midware() | ||
// Expose useful data in the route | ||
route.id = randomId(method, path) | ||
route.method = method.toUpperCase() | ||
// Register route in the toxy stack | ||
this.routes.push(route) | ||
// Creates toxy route-level middleware stacks | ||
route._rules = midware() | ||
route._poisons = midware() | ||
// Setup route middleware and final handler | ||
setupMiddleware(route) | ||
// Register route in the toxy stack | ||
self.routes.push(route) | ||
// Re-dispatch route if reaches the final handler | ||
route.use(function (req, res, next) { | ||
route.dispatcher.doDispatch(req, res, noop) | ||
}) | ||
// Setup route middleware and final handler | ||
setupMiddleware(route) | ||
reDispatchRoute(route) | ||
return route | ||
} | ||
return route | ||
} | ||
function reDispatchRoute(route) { | ||
// Re-dispatch the HTTP request if reaches the final handler! | ||
route.use(function (req, res, next) { | ||
route.dispatcher.doDispatch(req, res, noop) | ||
Toxy.prototype.findRoute = function (routeId, method) { | ||
if (method) routeId = randomId(method, routeId) | ||
var routes = this.routes.filter(function (route) { | ||
return route.unregistered !== true | ||
}).filter(function (route) { | ||
return route.id === routeId | ||
}) | ||
return routes.shift() | ||
} | ||
@@ -85,5 +82,5 @@ | ||
if (filter === true) return next() | ||
self._poisons.run(req, res, next) | ||
self._inPoisons.run(req, res, next) | ||
} | ||
}) | ||
} |
{ | ||
"name": "toxy", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"description": "Hackable HTTP proxy to simulate server failure scenarios and unexpected network conditions", | ||
@@ -5,0 +5,0 @@ "repository": "h2non/toxy", |
357
README.md
@@ -28,2 +28,4 @@ # toxy [![Build Status](https://api.travis-ci.org/h2non/toxy.svg?branch=master&style=flat)](https://travis-ci.org/h2non/toxy) [![Code Climate](https://codeclimate.com/github/h2non/toxy/badges/gpa.svg)](https://codeclimate.com/github/h2non/toxy) [![NPM](https://img.shields.io/npm/v/toxy.svg)](https://www.npmjs.org/package/toxy) | ||
- [Poisons](#poisons) | ||
- [Poisoning scopes](#poisoning-scopes) | ||
- [Poisoning phases](#poisoning-phases) | ||
- [Built-in poisons](#built-in-poisons) | ||
@@ -46,4 +48,7 @@ - [Latency](#latency) | ||
- [Headers](#headers) | ||
- [Response headers](#response-headers) | ||
- [Content Type](#content-type) | ||
- [Response status](#response-status) | ||
- [Body](#body) | ||
- [Response body](#response-body) | ||
- [How to write rules](#how-to-write-rules) | ||
@@ -67,6 +72,7 @@ - [Programmatic API](#programmatic-api) | ||
- Easily augmentable via middleware (based on connect/express middleware) | ||
- Supports both incoming and outgoing traffic poisioning | ||
- Built-in poisons (bandwidth, error, abort, latency, slow read...) | ||
- Rule-based poisoning (probabilistic, HTTP method, headers, body...) | ||
- Support third-party poisons and rules | ||
- Built-in balancer and traffic intercept via middleware | ||
- Supports third-party poisons and rules | ||
- Built-in balancer and traffic interceptor via middleware | ||
- Inherits API and features from [rocky](https://github.com/h2non/rocky) | ||
@@ -82,4 +88,7 @@ - Compatible with connect/express (and most of their middleware) | ||
toxy provides a powerful hackable and extensible solution with a convenient abstraction, but also a low-level interface and programmatic capabilities exposed as a simple, pluggable, concise and fluent API, and, for sure, with the implicit power, simplicity and fun of node.js. | ||
toxy provides a powerful hackable and extensible solution with a convenient abstraction, but without losing a convenient low-level interface capabilities to deal with HTTP protocol primitives properly. | ||
toxy was designed based on the rules of composition, simplicity and extensibility. | ||
Via its middleware layer you can easily augment toxy features to your own needs. | ||
### Concepts | ||
@@ -89,3 +98,3 @@ | ||
**Poisons** are the specific logic to infect an incoming or outgoing HTTP flow (e.g: injecting a latency, replying with an error). HTTP flow can be poisoned by one or multiple poisons, and poisons can be plugged to infect both global or route level incoming traffic. | ||
**Poisons** are the specific logic to infect an incoming or outgoing HTTP flow (e.g: injecting a latency, replying with an error). HTTP flow can be poisoned by one or multiple poisons, and poisons can be plugged to infect both global or route level traffic, and in both incoming and outgoing traffic flows. | ||
@@ -97,20 +106,39 @@ **Rules** are a kind of validation filters that can be reused and applied to global incoming HTTP traffic, route level traffic or into a specific poison. Their responsability is to determine, via inspecting each incoming HTTP request, if the registered poisons should be enabled or not, and therefore infecting or not the HTTP traffic (e.g: match headers, query params, method, body...). | ||
``` | ||
↓ ( Incoming request ) ↓ | ||
↓ ||| ↓ | ||
↓ ---------------- ↓ | ||
↓ | Toxy Router | ↓ --> Match the incoming request | ||
↓ ---------------- ↓ | ||
↓ ||| ↓ | ||
↓ ---------------- ↓ | ||
↓ | Exec Rules | ↓ --> Apply configured rules for the request | ||
↓ ---------------- ↓ | ||
↓ ||| ↓ | ||
↓ ---------------- ↓ | ||
↓ | Exec Poisons | ↓ --> If all rules passed, then poison the HTTP flow | ||
↓ ---------------- ↓ | ||
↓ / \ ↓ | ||
↓ \ / ↓ | ||
↓ ------------------- ↓ | ||
↓ | HTTP dispatcher | ↓ --> Proxy the HTTP traffic, either poisoned or not | ||
↓ ------------------- ↓ | ||
↓ ( Incoming request ) ↓ | ||
↓ ||| ↓ | ||
↓ --------------- ↓ | ||
↓ | Toxy Router | ↓ -> Match the incoming request | ||
↓ --------------- ↓ | ||
↓ ||| ↓ | ||
↓ |--------------------| ↓ | ||
↓ | Incoming phase | ↓ | ||
↓ |~~~~~~~~~~~~~~~~~~~~| ↓ | ||
↓ | ---------------- | ↓ | ||
↓ | | Exec Rules | | ↓ -> Apply configured rules for the incoming request | ||
↓ | ---------------- | ↓ | ||
↓ | ||| | ↓ | ||
↓ | ---------------- | ↓ | ||
↓ | | Exec Poisons | | ↓ -> If all rules passed, then poison the HTTP flow | ||
↓ | ---------------- | ↓ | ||
↓ |~~~~~~~~~~~~~~~~~~~~| ↓ | ||
↓ / \ ↓ | ||
↓ \ / ↓ | ||
↓ /--------------------\ ↓ | ||
↓ | HTTP dispatcher | ↓ -> Forward the HTTP traffic to the target server, either poisoned or not | ||
↓ \--------------------/ ↓ | ||
↓ / \ ↓ | ||
↓ \ / ↓ | ||
↓ |--------------------| ↓ | ||
↓ | Outgoing phase | ↓ -> Receives response from target server | ||
↓ |~~~~~~~~~~~~~~~~~~~~| ↓ | ||
↓ | ---------------- | ↓ | ||
↓ | | Exec Rules | | ↓ -> Apply configured rules for the outoing request | ||
↓ | ---------------- | ↓ | ||
↓ | ||| | ↓ | ||
↓ | ---------------- | ↓ | ||
↓ | | Exec Poisons | | ↓ -> If all rules passed, then poison the HTTP flow before send it to the client | ||
↓ | ---------------- | ↓ | ||
↓ |~~~~~~~~~~~~~~~~~~~~| ↓ | ||
↓ ||| ↓ | ||
↓ ( Send to the client ) ↓ -> Finally, send the request to the client, either poisoned or not | ||
``` | ||
@@ -154,5 +182,8 @@ | ||
// Infect outgoing traffic only (after the server replied properly) | ||
proxy | ||
.get('/image/*') | ||
.poison(poisons.bandwidth({ bps: 512 })) | ||
.outgoingPoison(poisons.bandwidth({ bps: 512 })) | ||
.withRule(rules.method('GET')) | ||
.withRule(rules.responseStatus({ range: [ 200, 400 ] })) | ||
@@ -182,3 +213,3 @@ proxy | ||
Poisons host specific logic which intercepts and mutates, wraps, modify and/or cancel an HTTP transaction in the proxy server. | ||
Poisons can be applied to incoming or outgoing, or even both traffic flows. | ||
Poisons can be applied to incoming or outgoing, or even both traffic flows (see [poison phases](#poisioning-phases)). | ||
@@ -188,7 +219,47 @@ Poisons can be composed and reused for different HTTP scenarios. | ||
### Poisoning scopes | ||
`toxy` has a hierarchical design. There're two different scopes: `global` and `route`. | ||
Poisons can be plugged to both scopes, meaning you can limit the scope of the poisioning, | ||
for instance, you might wanna apply a bandwidth limit poisioning only to | ||
a certain routes, such as `/download` or `/images`. | ||
See [routes.js](https://github.com/h2non/toxy/blob/master/examples/routes.js) for a featured example. | ||
### Poisoning phases | ||
Poisoning can be done to incoming or outgoing traffic flows, or even both. | ||
**Incoming** poisoning is applied when the traffic is still receiving by proxy | ||
and it has not been forwarded to the target server yet. | ||
**Outgoing** poisoning is applied when the traffic has been forwarded to the target server and | ||
the proxy recieves the response from it, but that response has not been send to the client yet. | ||
This is, essentially, that you can plug in your poisons to infect the HTTP traffic | ||
before or after the request is forwarded to the target HTTP server. | ||
This allows you apply a better and more accurated poisoning based on the server response. | ||
For instance, given the nature of some poisons, like `inject error`, | ||
you may require to enable it according to the target server response. | ||
See [poison-phases.js](https://github.com/h2non/toxy/blob/master/examples/poison-phases.js) for a featured example. | ||
### Built-in poisons | ||
#### Latency | ||
Name: `latency` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>latency</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Infects the HTTP flow injecting a latency jitter in the response | ||
@@ -210,12 +281,24 @@ | ||
#### Inject response | ||
Name: `inject` | ||
Injects a custom response, intercepting the request before sending it to the target server. Useful to inject errors originated in the server. | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>inject</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>false (only as incoming poison)</td> | ||
</tr> | ||
</table> | ||
Injects a custom response, intercepting the request before sending it to the target server. | ||
Useful to inject errors originated in the server. | ||
**Arguments**: | ||
- **options** `object` | ||
- **code** `number` - Response HTTP status code | ||
- **code** `number` - Response HTTP status code. Default `500` | ||
- **headers** `object` - Optional headers to send | ||
- **body** `mixed` - Optional body data to send | ||
- **body** `mixed` - Optional body data to send. It can be a `buffer` or `string` | ||
- **encoding** `string` - Body encoding. Default to `utf8` | ||
@@ -232,6 +315,17 @@ | ||
#### Bandwidth | ||
Name: `bandwidth` | ||
Limits the amount of bytes sent over the network in outgoing HTTP traffic for a specific threshold time frame. | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>bandwidth</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Limits the amount of bytes sent over the network in outgoing HTTP traffic for a specific time frame. | ||
This poison is basically an alias to [throttle](#throttle). | ||
@@ -242,12 +336,23 @@ | ||
- **options** `object` | ||
- **bps** `number` - Bytes per second. Default to `1024` | ||
- **threshold** `number` - Limit time frame in miliseconds. Default `1000` | ||
- **bytes** `number` - Amount of chunk of bytes to send. Default `1024` | ||
- **threshold** `number` - Packets time frame in miliseconds. Default `1000` | ||
```js | ||
toxy.poison(toxy.poisons.bandwidth({ bps: 512 })) | ||
toxy.poison(toxy.poisons.bandwidth({ bytes: 512 })) | ||
``` | ||
#### Rate limit | ||
Name: `rateLimit` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>rateLimit</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Limits the amount of requests received by the proxy in a specific threshold time frame. Designed to test API limits. Exposes typical `X-RateLimit-*` headers. | ||
@@ -261,6 +366,6 @@ | ||
- **options** `object` | ||
- **limit** `number` - Total amount of request. Default to `10` | ||
- **threshold** `number` - Limit threshold time frame in miliseconds. Default to `1000` | ||
- **message** `string` - Optional error message when limit reached. | ||
- **code** `number` - HTTP status code when limit reached. Default to `429`. | ||
- **limit** `number` - Total amount of requests. Default to `10` | ||
- **threshold** `number` - Limit time frame in miliseconds. Default to `1000` | ||
- **message** `string` - Optional error message when limit is reached. | ||
- **code** `number` - HTTP status code when limit is reached. Default to `429`. | ||
@@ -272,4 +377,15 @@ ```js | ||
#### Slow read | ||
Name: `slowRead` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>rateLimit</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Reads incoming payload data packets slowly. Only valid for non-GET request. | ||
@@ -290,2 +406,14 @@ | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>slowOpen</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Delays the HTTP connection ready state. | ||
@@ -303,4 +431,15 @@ | ||
#### Slow close | ||
Name: `slowClose` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>slowClose</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Delays the HTTP connection close signal (EOF). | ||
@@ -318,4 +457,15 @@ | ||
#### Throttle | ||
Name: `throttle` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>throttle</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Restricts the amount of packets sent over the network in a specific threshold time frame. | ||
@@ -327,3 +477,3 @@ | ||
- **chunk** `number` - Packet chunk size in bytes. Default to `1024` | ||
- **threshold** `object` - Limit threshold time frame in miliseconds. Default to `100` | ||
- **threshold** `object` - Limit threshold time frame in miliseconds. Default to `1000` | ||
@@ -335,4 +485,15 @@ ```js | ||
#### Abort connection | ||
Name: `abort` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>slowClose</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>false (only as incoming poison)</td> | ||
</tr> | ||
</table> | ||
Aborts the TCP connection. From the low-level perspective, this will destroy the socket on the server, operating only at TCP level without sending any specific HTTP application level data. | ||
@@ -350,4 +511,15 @@ | ||
#### Timeout | ||
Name: `timeout` | ||
<table> | ||
<tr> | ||
<td><b>Name</b></td><td>timout</td> | ||
</tr> | ||
<tr> | ||
<td><b>Poisioning Phase</b></td><td>incoming / outgoing</td> | ||
</tr> | ||
<tr> | ||
<td><b>Reaches the server</b></td><td>true</td> | ||
</tr> | ||
</table> | ||
Defines a response timeout. Useful when forward to potentially slow servers. | ||
@@ -451,3 +623,3 @@ | ||
Filter by certain headers. | ||
Filter by request headers. | ||
@@ -471,2 +643,23 @@ **Arguments**: | ||
#### Response headers | ||
Filter by response headers from target server. Same as `headers` rule, but evaluating the outgoing request. | ||
**Arguments**: | ||
- **headers** `object` - Headers to match by key-value pair. `value` can be a string, regexp, `boolean` or `function(headerValue, headerName) => boolean` | ||
```js | ||
var matchHeaders = { | ||
'content-type': /^application/\json/i, | ||
'server': true, // meaning it should be present, | ||
'accept': function (value, key) { | ||
return value.indexOf('text') !== -1 | ||
} | ||
} | ||
var rule = toxy.rules.responseHeaders(matchHeaders) | ||
toxy.rule(rule) | ||
``` | ||
#### Content Type | ||
@@ -485,6 +678,24 @@ | ||
#### Response status | ||
Evaluates the response status from the target server. | ||
Only applicable to outgoing poisons. | ||
**Arguments**: | ||
- **range** `array` - Pair of status code range. Default to `[200, 400]`. | ||
- **value** `string|regexp` - Header value to match. | ||
```js | ||
var rule = toxy.rules.contentType('application/json') | ||
toxy.rule(rule) | ||
``` | ||
#### Body | ||
Match incoming body payload data by string, regexp or custom filter function | ||
Match incoming body payload by a given `string`, `regexp` or custom filter `function`. | ||
This rule is pretty simple, so for complex body matching (e.g: validating againts a JSON schema) | ||
you should probably write your own rule. | ||
**Arguments**: | ||
@@ -502,3 +713,3 @@ | ||
// Or using a filter function returning a boolean | ||
var rule = toxy.rules.body(function (body) { | ||
var rule = toxy.rules.body(function contains(body) { | ||
return body.indexOf('hello') !== -1 | ||
@@ -509,2 +720,23 @@ }) | ||
#### Response body | ||
Match outgoing body payload by a given `string`, `regexp` or custom filter `function`. | ||
**Arguments**: | ||
- **match** `string|regexp|function` - Body content to match | ||
- **encoding** `string` - Body encoding. Default to `utf8` | ||
- **length** `number` - Body length. Default taken from `Content-Length` header | ||
```js | ||
var rule = toxy.rules.responseBody('"hello":"world"') | ||
toxy.rule(rule) | ||
// Or using a filter function returning a boolean | ||
var rule = toxy.rules.responseBody(function contains(body) { | ||
return body.indexOf('hello') !== -1 | ||
}) | ||
toxy.rule(rule) | ||
``` | ||
### How to write rules | ||
@@ -580,3 +812,11 @@ | ||
toxy | ||
.post('/boo') | ||
.outgoingPoison(toxy.poisons.bandwidth({ bps: 1024 })) | ||
.withRule(toxy.rules.method('GET')) | ||
.forward('http://boo.server') | ||
toxy.all('/*') | ||
toxy.listen(3000) | ||
``` | ||
@@ -660,2 +900,14 @@ | ||
#### toxy#requestBody(middleware) | ||
Intercept incoming request body. Useful to modify it on the fly. | ||
For more information, see the [rocky docs](https://github.com/h2non/rocky#programmatic-api) | ||
#### toxy#responseBody(middleware) | ||
Intercept outgoing response body. Useful to modify it on the fly. | ||
For more information, see the [rocky docs](https://github.com/h2non/rocky#programmatic-api) | ||
#### toxy#middleware() | ||
@@ -680,4 +932,9 @@ | ||
Register a new poison. | ||
Register a new poison to be applied to [incoming](#poisioning-phases) traffic. | ||
#### toxy#outgoingPoison(poison) | ||
Alias: `useOutgoingPoison` | ||
Register a new poison to be applied to [outgoing](#poisioning-phases) traffic. | ||
#### toxy#rule(rule) | ||
@@ -950,2 +1207,3 @@ Alias: `useRule` | ||
"name": "latency", | ||
"phase": "outgoing", | ||
"options": { "jitter": 1000 } | ||
@@ -1034,2 +1292,3 @@ } | ||
"name": "latency", | ||
"phase": "outgoing", | ||
"options": { "jitter": 1000 } | ||
@@ -1036,0 +1295,0 @@ } |
@@ -5,3 +5,3 @@ const fs = require('fs') | ||
const spawn = require('child_process').spawn | ||
const eachSeries = require('../lib/common').eachSeries | ||
const eachSeries = require('../lib/helpers').eachSeries | ||
@@ -8,0 +8,0 @@ const rootDir = path.join(__dirname, '..') |
@@ -29,3 +29,8 @@ const expect = require('chai').expect | ||
var opts = { body: 'Hello', encoding: 'utf8' } | ||
var expected = { code: 500, body: 'Hello', encoding: 'utf8' } | ||
var expected = { | ||
code: 500, | ||
body: 'Hello', | ||
encoding: 'utf8', | ||
headers: { 'content-length': 5 } | ||
} | ||
@@ -49,2 +54,28 @@ var res = {} | ||
}) | ||
test('merge headers', function (done) { | ||
var opts = { headers: { server: 'toxy' } } | ||
var expected = { | ||
code: 500, | ||
headers: { server: 'toxy', foo: 'bar' } | ||
} | ||
var res = {} | ||
res.headers = { server: 'nginx', foo: 'bar' } | ||
res.writeHead = writeHead | ||
res.end = end | ||
inject(opts)(null, res) | ||
function writeHead(code, headers) { | ||
expect(code).to.be.equal(expected.code) | ||
expect(headers).to.be.deep.equal(expected.headers) | ||
} | ||
function end(data, encoding) { | ||
expect(data).to.be.equal(expected.body) | ||
expect(encoding).to.be.equal(expected.encoding) | ||
done() | ||
} | ||
}) | ||
}) |
122
test/toxy.js
@@ -8,6 +8,10 @@ const http = require('http') | ||
suite('toxy', function () { | ||
test('static members', function () { | ||
test('public static members', function () { | ||
expect(toxy.rules).to.be.an('object') | ||
expect(toxy.poisons).to.be.an('object') | ||
expect(toxy.Directive).to.be.a('function') | ||
expect(toxy.Poison).to.be.a('function') | ||
expect(toxy.Rule).to.be.a('function') | ||
expect(toxy.Rocky).to.be.a('function') | ||
expect(toxy.admin).to.be.a('function') | ||
expect(toxy.VERSION).to.be.a('string') | ||
@@ -18,6 +22,6 @@ }) | ||
var proxy = toxy() | ||
var called = false | ||
var spy = sinon.spy() | ||
proxy.poison(function delay(req, res, next) { | ||
called = true | ||
spy(req, res) | ||
setTimeout(next, 5) | ||
@@ -32,4 +36,4 @@ }) | ||
proxy._poisons.run(null, null, function () { | ||
expect(called).to.be.true | ||
proxy._inPoisons.run(null, null, function () { | ||
expect(spy.calledOnce).to.be.true | ||
done() | ||
@@ -39,2 +43,46 @@ }) | ||
test('use poison phases', function (done) { | ||
var proxy = toxy() | ||
var spy = sinon.spy() | ||
proxy.poison(function delay(req, res, next) { | ||
spy(req, res) | ||
next() | ||
}) | ||
proxy.outgoingPoison(function delay(req, res, next) { | ||
spy(req, res) | ||
next() | ||
}) | ||
expect(proxy.isEnabled('delay')).to.be.true | ||
proxy.disable('delay') | ||
expect(proxy.isEnabled('delay')).to.be.false | ||
expect(proxy.isEnabledOutgoing('delay')).to.be.true | ||
proxy.enable('delay') | ||
expect(proxy.isEnabled('delay')).to.be.true | ||
proxy.disableOutgoing('delay') | ||
expect(proxy.isEnabled('delay')).to.be.true | ||
expect(proxy.isEnabledOutgoing('delay')).to.be.false | ||
proxy.enableOutgoing('delay') | ||
proxy._inPoisons.run(null, null, function () { | ||
expect(spy.calledOnce).to.be.true | ||
}) | ||
proxy._outPoisons.run(null, null, function () { | ||
expect(spy.calledTwice).to.be.true | ||
proxy.remove('delay') | ||
expect(proxy.isEnabled('delay')).to.be.false | ||
expect(proxy.isEnabledOutgoing('delay')).to.be.true | ||
proxy.removeOutgoing('delay') | ||
expect(proxy.isEnabled('delay')).to.be.false | ||
expect(proxy.isEnabledOutgoing('delay')).to.be.false | ||
done() | ||
}) | ||
}) | ||
test('use rule', function (done) { | ||
@@ -125,3 +173,3 @@ var proxy = toxy() | ||
test('basic proxy', function (done) { | ||
test('basic proxy with poisons', function (done) { | ||
var proxy = toxy() | ||
@@ -157,5 +205,7 @@ var spy = sinon.spy() | ||
expect(spy.calledTwice).to.be.true | ||
expect(spy.args[0][0].url).to.be.equal('/foo') | ||
expect(spy.args[0][0].method).to.be.equal('GET') | ||
var req = spy.args[0][0] | ||
expect(req.url).to.be.equal('/foo') | ||
expect(req.method).to.be.equal('GET') | ||
server.close() | ||
@@ -166,2 +216,53 @@ proxy.close(done) | ||
test('proxy with outgoing poisons', function (done) { | ||
var proxy = toxy() | ||
var spy = sinon.spy() | ||
var server = createServer(9081, 200) | ||
var timeout = 100 | ||
proxy.outgoingPoison(function delay(req, res, next) { | ||
spy(req, res) | ||
setTimeout(next, timeout) | ||
}) | ||
proxy.outgoingPoison(function capture(req, res, next) { | ||
spy(req, res) | ||
next() | ||
}) | ||
proxy.rule(function method(req, res, next) { | ||
spy(req, res) | ||
next(null, req.method !== 'GET') | ||
}) | ||
proxy.forward('http://localhost:9081') | ||
proxy.get('/foo') | ||
proxy.listen(9080) | ||
var init = Date.now() | ||
supertest('http://localhost:9080') | ||
.get('/foo') | ||
.expect(200) | ||
.expect('Content-Type', 'application/json') | ||
.expect({ hello: 'world' }) | ||
.end(assert) | ||
function assert(err) { | ||
expect(Date.now() - init).to.be.at.least(timeout - 1) | ||
expect(spy.calledThrice).to.be.true | ||
var req = spy.args[0][0] | ||
expect(req.url).to.be.equal('/foo') | ||
expect(req.method).to.be.equal('GET') | ||
var res = spy.args[0][1] | ||
expect(res.getHeader('server')).to.be.deep.equal('rocky') | ||
expect(res._originalBody.toString()).to.be.deep.equal('{"hello":"world"}') | ||
expect(res.body.toString()).to.be.deep.equal('{"hello":"world"}') | ||
server.close() | ||
proxy.close(done) | ||
} | ||
}) | ||
test('final route handler when no matches', function (done) { | ||
@@ -196,4 +297,5 @@ var proxy = toxy() | ||
expect(spy.calledOnce).to.be.true | ||
expect(spy.args[0][0].url).to.be.equal('/foo') | ||
expect(spy.args[0][0].method).to.be.equal('GET') | ||
var req = spy.args[0][0] | ||
expect(req.url).to.be.equal('/foo') | ||
expect(req.method).to.be.equal('GET') | ||
done(err) | ||
@@ -200,0 +302,0 @@ } |
Sorry, the diff of this file is not supported yet
144156
100
3563
1366
12