Comparing version 0.1.0-alpha.0 to 0.1.0
33
index.js
@@ -7,2 +7,6 @@ const Toxy = require('./lib/toxy') | ||
/** | ||
* API factory | ||
*/ | ||
function toxy(opts) { | ||
@@ -12,9 +16,23 @@ return new Toxy(opts) | ||
toxy.Poison = require('./lib/poison') | ||
/** | ||
* Expose internal modules as static members | ||
*/ | ||
toxy.Rule = require('./lib/rule') | ||
toxy.Base = require('./lib/base') | ||
toxy.Directive = require('./lib/directive') | ||
toxy.VERSION = require('./package.json').version | ||
toxy.Rocky = require('rocky').Rocky | ||
toxy.poisons = | ||
Toxy.prototype.poisons = Object.create(null) | ||
/** | ||
* Expose current version | ||
*/ | ||
toxy.VERSION = require('./package.json').version | ||
/** | ||
* Expose built-in poisons | ||
*/ | ||
toxy.poisons = Toxy.prototype.poisons = Object.create(null) | ||
poisons.forEach(function (poison) { | ||
@@ -26,5 +44,8 @@ Toxy.prototype.poisons[poison.name] = function () { | ||
toxy.rules = | ||
Toxy.prototype.rules = Object.create(null) | ||
/** | ||
* Expose built-in rules | ||
*/ | ||
toxy.rules = Toxy.prototype.rules = Object.create(null) | ||
rules.forEach(function (rule) { | ||
@@ -31,0 +52,0 @@ Toxy.prototype.rules[rule.name] = function () { |
@@ -1,2 +0,2 @@ | ||
const midware = require('midware') | ||
const Rule = require('./rule') | ||
@@ -6,25 +6,28 @@ module.exports = Directive | ||
function Directive(directive) { | ||
if (typeof directive !== 'function') { | ||
throw new TypeError('First argument must be a function') | ||
} | ||
Rule.call(this) | ||
this.enabled = true | ||
this.directive = directive | ||
this._rules = midware() | ||
} | ||
Directive.prototype.isEnabled = function () { | ||
return this.disabled !== false | ||
} | ||
Directive.prototype = Object.create(Rule.prototype) | ||
Directive.prototype.disable = function () { | ||
return this.disabled = true | ||
return this.enabled = false | ||
} | ||
Directive.prototype.enable = function () { | ||
return this.disabled = false | ||
return this.enabled = true | ||
} | ||
Directive.prototype.rule = function (rule) { | ||
var r = new Directive(rule) | ||
this._rules(r.handler()) | ||
Directive.prototype.isEnabled = function () { | ||
return this.enabled | ||
} | ||
Directive.prototype.rule = | ||
Directive.prototype.filter = function (rule) { | ||
if (!(rule instanceof Directive)) { | ||
rule = new Directive(rule) | ||
} | ||
this._rules(rule.handler()) | ||
return this | ||
@@ -37,3 +40,3 @@ } | ||
function handler(req, res, next) { | ||
if (self.disabled === true) return next() | ||
if (self.enabled === false) return next() | ||
self._rules.run(req, res, handle.bind(self)) | ||
@@ -40,0 +43,0 @@ |
module.exports = [ | ||
require('./delay'), | ||
require('./inject'), | ||
require('./abort'), | ||
require('./latency'), | ||
require('./throttle'), | ||
@@ -6,0 +6,0 @@ require('./timeout'), |
module.exports = function rateLimit(opts) { | ||
opts = opts || {} | ||
var limit = +opts.limit || 1 | ||
var limit = +opts.limit || 10 | ||
var threshold = +opts.threshold || 1000 | ||
@@ -5,0 +5,0 @@ var message = opts.message || 'Too many requests' |
@@ -10,4 +10,5 @@ const defaultDelay = 5000 | ||
var _end = res.end | ||
var _setHeader = res.setHeader | ||
var _writeHead = res.writeHead | ||
var resproto = Object.getPrototypeOf(res) | ||
var _setHeader = resproto.setHeader | ||
var _writeHead = resproto.writeHead | ||
@@ -45,3 +46,3 @@ res.setHeader = function (header, value) { | ||
_res = _setHeader = null | ||
_writeHead = args = null | ||
_writeHead = args = resproto = null | ||
} | ||
@@ -48,0 +49,0 @@ } |
@@ -5,3 +5,4 @@ const common = require('../common') | ||
opts = opts || {} | ||
var threshold = +opts.threshold || 100 | ||
var threshold = +opts.threshold || 1000 | ||
var chunkSize = (+opts.chunk || +opts.bps) || 1024 | ||
@@ -23,8 +24,8 @@ | ||
req.push = function (data, encoding) { | ||
if (ended) return | ||
if (data === null) return writeStream() | ||
if (data === null) return writeChunks() | ||
common.sliceBuffer(chunkSize, data, encoding, buf) | ||
} | ||
function writeStream() { | ||
function writeChunks() { | ||
if (ended) return | ||
ended = true | ||
@@ -35,4 +36,5 @@ common.eachSeries(buf, pushDefer, end) | ||
function end() { | ||
if (closed) return | ||
cleanup() | ||
if (!closed) req.push(null) | ||
req.push(null) | ||
} | ||
@@ -39,0 +41,0 @@ |
108
lib/proxy.js
const rocky = require('rocky') | ||
const Base = rocky.Base | ||
const RockyBase = rocky.Base | ||
const Rule = require('./rule') | ||
const rules = require('./rules') | ||
const poisons = require('./poisons') | ||
const Directive = require('./directive') | ||
const Poison = require('./poison') | ||
module.exports = rocky.Rocky | ||
Base.prototype.poison = | ||
Base.prototype.usePoison = function (poison) { | ||
if (poison instanceof Poison) { | ||
this._poisons(poison.handler()) | ||
return poison | ||
RockyBase.prototype.rule = | ||
RockyBase.prototype.filter = Directive.prototype.rule | ||
RockyBase.prototype.withRule = | ||
RockyBase.prototype.poisonRule = | ||
RockyBase.prototype.poisonFilter = function (rule) { | ||
if (this.lastPoison) { | ||
this.lastPoison.rule(rule) | ||
} | ||
var p = new Poison(poison) | ||
this._poisons(p.handler()) | ||
this.lastPoison = p | ||
return this | ||
} | ||
Base.prototype.rule = | ||
Base.prototype.filter = Directive.prototype.rule | ||
Base.prototype.poisonRule = | ||
Base.prototype.poisonFilter = function (rule) { | ||
if (!this.lastPoison) return this | ||
this.lastPoison.rule(rule) | ||
return this | ||
RockyBase.prototype.enable = function (poison) { | ||
return this._callMethod(this._poisons, 'enable', poison) | ||
} | ||
Base.prototype.disable = function (poison) { | ||
return stackCallProxy(this._poisons.stack, 'disable', poison) | ||
RockyBase.prototype.disable = function (poison) { | ||
return this._callMethod(this._poisons, 'disable', poison) | ||
} | ||
Base.prototype.enable = function (poison) { | ||
return stackCallProxy(this._poisons.stack, 'enable', poison) | ||
RockyBase.prototype.remove = function (poison) { | ||
return this._remove(this_poisons, poison) | ||
} | ||
Base.prototype.enableRule = function (rule) { | ||
return stackCallProxy(this._rules.stack, 'enable', rule) | ||
RockyBase.prototype.isEnabled = function (poison) { | ||
return this._callMethod(this._poisons, 'isEnabled', poison) | ||
} | ||
Base.prototype.disableRule = function (rule) { | ||
return stackCallProxy(this._rules.stack, 'disable', rule) | ||
} | ||
Base.prototype.isPoisonEnabled = function (poison) { | ||
return stackCallProxy(this._poisons.stack, 'isEnabled', poison) | ||
} | ||
Base.prototype.isRuleEnabled = function (rule) { | ||
return stackCallProxy(this._rules.stack, 'isEnabled', rule) | ||
} | ||
Base.prototype.disableAll = function () { | ||
RockyBase.prototype.disableAll = | ||
RockyBase.prototype.disablePoisons = function () { | ||
return this._disableAll(this._poisons) | ||
} | ||
Base.prototype.disableAllRules = function () { | ||
return this._disableAll(this._rules) | ||
RockyBase.prototype.poisons = | ||
RockyBase.prototype.getPoisons = function () { | ||
return this._getAll(this._poisons) | ||
} | ||
Base.prototype._disableAll = function (mw) { | ||
mw.stack.forEach(function (fn) { | ||
fn.$of.disable() | ||
}) | ||
RockyBase.prototype.flush = | ||
RockyBase.prototype.flushPoisons = function () { | ||
this._poisons.stack.splice(0) | ||
return this | ||
} | ||
Base.prototype.getPoisons = function () { | ||
return this._poisons.stack.map(function (fn) { | ||
return fn.$of | ||
}) | ||
} | ||
RockyBase.prototype.poison = | ||
RockyBase.prototype.usePoison = function (poison) { | ||
if (!(poison instanceof Directive)) { | ||
poison = new Directive(poison) | ||
} | ||
Base.prototype.getRules = function () { | ||
return this._rules.stack.map(function (fn) { | ||
return fn.$of | ||
}) | ||
} | ||
Base.prototype.flushAll = | ||
Base.prototype.flushAllPoisons = function () { | ||
this._poisons.stack.splice(0) | ||
this._poisons(poison.handler()) | ||
this.lastPoison = poison | ||
return this | ||
} | ||
Base.prototype.flushAllRules = function () { | ||
this._rules.stack.splice(0) | ||
return this | ||
} | ||
function stackCallProxy(stack, action, name) { | ||
for (var i = 0, l = stack.length; i < l; i += 1) { | ||
if (stack[i].$name === name || stack[i].$of === name) { | ||
return stack[i].$of[action]() | ||
} | ||
} | ||
return false | ||
} | ||
Object.keys(Rule.prototype).forEach(function (key) { | ||
RockyBase.prototype[key] = Rule.prototype[key] | ||
}) |
module.exports = [ | ||
require('./method'), | ||
require('./random'), | ||
require('./match-headers'), | ||
require('./probability'), | ||
require('./headers'), | ||
require('./content-type'), | ||
] |
@@ -1,16 +0,14 @@ | ||
module.exports = function method(opts) { | ||
if (typeof opts === 'string') { | ||
opts = { method: opts } | ||
module.exports = function method(matchMethod) { | ||
if (typeof matchMethod === 'string') { | ||
matchMethod = [ matchMethod ] | ||
} | ||
opts = opts || {} | ||
opts.method = opts.method || '' | ||
if (!matchMethod) { | ||
matchMethod = [ 'GET' ] | ||
} | ||
return function method(req, res, next) { | ||
var filter = req.method === opts.method.toUpperCase() | ||
|| (Array.isArray(opts.methods) | ||
&& !!~opts.methods.indexOf(req.method)) | ||
next(!filter) | ||
var ignore = !!~matchMethod.indexOf(req.method) | ||
next(!ignore) | ||
} | ||
} |
@@ -17,6 +17,6 @@ const midware = require('midware') | ||
setupMiddleware(this) | ||
wrapRoute(this) | ||
wrapRouteConstructor(this) | ||
} | ||
function wrapRoute(self) { | ||
function wrapRouteConstructor(self) { | ||
var _route = self.route | ||
@@ -27,3 +27,3 @@ self.route = function (method, path) { | ||
route._poisons = midware() | ||
setupMiddleware(route) | ||
setupMiddleware(route, self) | ||
return route | ||
@@ -35,2 +35,6 @@ } | ||
self.use(function (req, res, next) { | ||
// Expose the toxy instance via the middleware | ||
req.toxy = self | ||
// Run rules middleware validations before apply the poisons | ||
self._rules.run(req, res, function (filter) { | ||
@@ -37,0 +41,0 @@ if (filter === true) return next() |
{ | ||
"name": "toxy", | ||
"version": "0.1.0-alpha.0", | ||
"description": "Hackable HTTP proxy to simulate multiple server failures and unexpected conditions", | ||
"version": "0.1.0", | ||
"description": "Hackable HTTP proxy to simulate server failure scenarios and unexpected conditions", | ||
"repository": "h2non/toxy", | ||
@@ -41,2 +41,4 @@ "author": "Tomas Aparicio", | ||
"chai": "^3.0.0", | ||
"clone": "^1.0.2", | ||
"express": "^4.13.2", | ||
"mocha": "^2.2.5", | ||
@@ -43,0 +45,0 @@ "sinon": "^1.15.3", |
461
README.md
@@ -1,48 +0,94 @@ | ||
# 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) | ||
# 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) ![Stability](http://img.shields.io/badge/stability-beta-orange.svg?style=flat) | ||
<!-- | ||
![Downloads](https://img.shields.io/npm/dm/toxy.svg) | ||
--> | ||
<img align="right" height="180" src="http://s8.postimg.org/ikc9jxllh/toxic.jpg" /> | ||
Pluggable and hackable HTTP proxy to simulate multiple server failures and unexpected conditions. | ||
Built for [node.js](http://nodejs.org)/[io.js](https://iojs.org). Powered by [rocky](https://github.com/h2non/rocky) | ||
**toxy** is a **hackable HTTP proxy** to **simulate** server **failure scenarios** and **unexpected conditions**. | ||
It was mainly created for fuzz/evil testing purposes. | ||
It becomes particulary useful in fault tolerant and resilient systems, tipically in service-oriented distributed architectures, where `toxy` may act as intermediate proxy among services. | ||
**toxy** allows you to plug in [poisons](#poisons), optionally filtered by [rules](#rules), which basically can intercept and alter the HTTP flow as you want performing multiple actions in the middle of that process, like for instance limiting the bandwidth, injecting a TCP jitter or replying with a custom error or status code. | ||
Runs in [node.js](http://nodejs.org)/[io.js](https://iojs.org). Compatible with [connect](https://github.com/senchalabs/connect)/[express](http://expressjs.com). | ||
Built on top of [rocky](https://github.com/h2non/rocky), a full-featured, middleware-oriented HTTP/S proxy. | ||
Requires node.js +0.12 or io.js +1.6 | ||
**This is a work in progress** | ||
## Contents | ||
## Built-in poisons | ||
- [Features](#features) | ||
- [Introduction](#introduction) | ||
- [Why toxy?](#why-toxy) | ||
- [Concepts](#concepts) | ||
- [How it works](#how-it-works) | ||
- [Usage](#usage) | ||
- [Installation](#installation) | ||
- [Examples](#examples) | ||
- [Poisons](#poisons) | ||
- [Built-in poisons](#build-in-poisons) | ||
- [How to write poisons](#how-to-write-poisons) | ||
- [Rules](#rules) | ||
- [Built-in rules](#built-in-rules) | ||
- [How to write rules](#how-to-write-rules) | ||
- [Programmatic API](#programmatic-api) | ||
- [License](#license) | ||
- [x] [Delay](#delay) | ||
- [x] [Timeout](#timeout) | ||
- [x] [Inject response](#inject-response) | ||
- [x] [Bandwidth](#bandwidth) | ||
- [x] [Rate limit](#rate-limit) | ||
- [x] [Slow read](#slow-read) | ||
- [x] [Slow open](#slow-open) | ||
- [x] [Slow close](#slow-close) | ||
- [x] [Throttle](#throttle) | ||
- [x] [Abort connection](#abort-connection) | ||
## Features | ||
## Built-in rules | ||
- Full-featured HTTP/S proxy (backed by [http-proxy](https://github.com/nodejistu/node-http-proxy)) | ||
- Hackable and elegant programmatic API (inspired on connect/express) | ||
- Featured built-in router with nested configuration | ||
- Hierarchical middleware layer (global and route-specific) | ||
- Easily augmentable via middleware (based on connect/express middleware) | ||
- 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 | ||
- Inherits the API and features from [rocky](https://github.com/h2non/rocky) | ||
- Compatible with connect/express (and most of their middleware) | ||
- Runs as standalone HTTP proxy | ||
- [x] [Random](#random) | ||
- [x] [Method](#method) | ||
- [x] [Headers](#headers) | ||
- [x] [Content Type](#content-type) | ||
- [ ] [Query params](#query-params) | ||
- [ ] [Body](#body) | ||
## Introduction | ||
<!-- | ||
## How it works | ||
### Why toxy? | ||
``` | ||
There're some other similar solutions to `toxy` in the market, but most of them don't provide a proper programmatic control and are not easy to hack, configure and/or extend. | ||
`toxy` provides a powerful hacking-driven solution with a proper low-level interface and programmatic control with an elegant API and the power, simplicity and fun of node.js. | ||
### Concepts | ||
`toxy` introduces two main core directives worth knowing before using it: | ||
**Poisons** are a specific logic encapsulated as middleware to poison an incoming or outgoing HTTP flow (e.g: injecting a latency in the server response). HTTP flow can be poisoned by one or multiple poisons. | ||
**Rules** are a kind of validation filters that can be applied to the whole HTTP flow or to a concrete poison, in order to determine if one or multiple poisons should be enabled or not to poison the HTTP traffic (e.g: a probabilistic calculus). | ||
### How it works | ||
``` | ||
--> | ||
↓ ( Incoming request ) ↓ | ||
↓ ||| ↓ | ||
↓ ---------------- ↓ | ||
↓ | Toxy Router | ↓ --> Match a route based on the incoming request | ||
↓ ---------------- ↓ | ||
↓ ||| ↓ | ||
↓ ---------------- ↓ | ||
↓ | Exec Rules | ↓ --> Apply configured rules for the request | ||
↓ ---------------- ↓ | ||
↓ ||| ↓ | ||
↓ ---------------- ↓ | ||
↓ | Exec Poisons | ↓ --> If all rules passed, poisoning the HTTP flow | ||
↓ ---------------- ↓ | ||
↓ / \ ↓ | ||
↓ \ / ↓ | ||
↓ ------------------- ↓ | ||
↓ | HTTP dispatcher | ↓ --> Proxy the HTTP traffic for both poisoned or not | ||
↓ ------------------- ↓ | ||
``` | ||
## Installation | ||
## Usage | ||
### Installation | ||
``` | ||
@@ -52,15 +98,26 @@ npm install toxy | ||
## Examples | ||
### Examples | ||
See the [examples](https://github.com/h2non/toxy/tree/master/examples) directory for more use cases | ||
#### Basic poisioning | ||
```js | ||
var toxy = require('toxy') | ||
var poisons = toxy.poisons | ||
var rules = toxy.rules | ||
var proxy = toxy() | ||
var poisons = proxy.poisons | ||
var rules = proxy.rules | ||
proxy | ||
.poison(poisons.delay({ jitter: 500 })) | ||
.forward('http://httpbin.org') | ||
proxy | ||
.poison(poisons.latency({ jitter: 500 })) | ||
.rule(rules.random(50)) | ||
.rule(rules.method('GET')) | ||
.poison(poisons.bandwidth({ bps: 1024 })) | ||
.withRule(rules.method('GET')) | ||
proxy.get('/*') | ||
proxy.listen(3000) | ||
``` | ||
@@ -70,38 +127,362 @@ | ||
#### Delay | ||
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. | ||
#### Timeout | ||
Poisons can be composed and reused for different HTTP scenarios. | ||
Poisons are executed in FIFO order. | ||
### Built-in poisons | ||
- [x] [Latency](#latency) | ||
- [x] [Inject response](#inject-response) | ||
- [x] [Bandwidth](#bandwidth) | ||
- [x] [Rate limit](#rate-limit) | ||
- [x] [Slow read](#slow-read) | ||
- [x] [Slow open](#slow-open) | ||
- [x] [Slow close](#slow-close) | ||
- [x] [Throttle](#throttle) | ||
- [x] [Abort connection](#abort-connection) | ||
- [x] [Timeout](#timeout) | ||
#### Latency | ||
Name: `latency` | ||
Injects response latency | ||
#### Inject response | ||
Name: `inject` | ||
Injects a custom response, intercepting the request before sending it to the target server. Useful to test server originated errors. | ||
#### Bandwidth | ||
Name: `bandwidth` | ||
Limits the amount of bytes sent over the network in a specific threshold time frame. | ||
#### Rate limit | ||
Name: `rateLimit` | ||
Rates the amount of requests received by the proxy in a specific threshold time frame. Designed to test API limits. | ||
#### Slow read | ||
Name: `slowRead` | ||
Reads incoming packets slowly. | ||
#### Slow open | ||
Name: `slowOpen` | ||
Delays the HTTP connection opened status. | ||
#### Slow close | ||
Name: `slowClose` | ||
Delays the EOF signal. | ||
#### Throttle | ||
Name: `throttle` | ||
Restricts the amount of packets sent over the network in a specific threshold time frame. | ||
#### Abort connection | ||
Name: `abort` | ||
Aborts the TCP connection, optionally with a custom error. 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. | ||
#### Timeout | ||
Name: `timeout` | ||
Defines a response timeout. Useful when forwarding to potentially slow servers. | ||
### How to write poisons | ||
Poisons are implemented as standalone middleware (like in connect/express). | ||
Here's a simple example of a server latency poison: | ||
```js | ||
function latency(delay) { | ||
/** | ||
* We name the function since toxy uses it as identifier to get/disable/remove it in the future | ||
*/ | ||
return function latency(req, res, next) { | ||
var timeout = setTimeout(clean, delay) | ||
req.once('close', onClose) | ||
function onClose() { | ||
clearTimeout(timeout) | ||
next('client connection closed') | ||
} | ||
function clean() { | ||
req.removeListener('close', onClose) | ||
next() | ||
} | ||
} | ||
} | ||
// Register and enable the poison | ||
toxy | ||
.get('/foo') | ||
.poison(latency(2000)) | ||
``` | ||
For a real example, take a look to the [built-in poisons](https://github.com/h2non/toxy/tree/master/lib/poisons) implementation. | ||
## Rules | ||
#### Random | ||
Rules are simple validation filters which inspect an HTTP request and determine, given a certain rules (e.g: method, headers, query params), if the HTTP transaction should be poisoned or not. | ||
Rules are useful to compose, decouple and reuse logic among different scenarios of poisoning. You can also define globally applied rules or nested poison-scope rules only. | ||
Rules are executed in FIFO order. Their evaluation logic is comparable to `Array#every()` in JavaScript: all the rules must match in order to proceed with the poisoning. | ||
### Built-in rules | ||
- [x] [Probability](#Probability) | ||
- [x] [Method](#method) | ||
- [x] [Headers](#headers) | ||
- [x] [Content Type](#content-type) | ||
- [ ] [Body](#body) | ||
#### Probability | ||
Enables the rule by a random probabilistic. Useful for random poisioning. | ||
**Arguments**: | ||
- **percentage** `number` - Percentage of filtering. Default `50` | ||
```js | ||
var rule = toxy.rules.probability(85) | ||
toxy.rule(rule) | ||
``` | ||
#### Method | ||
Filters by HTTP method. | ||
**Arguments**: | ||
- **method** `string|array` - Method or methods to filter. | ||
```js | ||
var method = toxy.rules.method(['GET', 'POST']) | ||
toxy.rule(method) | ||
``` | ||
#### Headers | ||
Filter by certain headers. | ||
**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.headers(matchHeaders) | ||
toxy.rule(rule) | ||
``` | ||
#### Content Type | ||
#### Query params | ||
Filters by content type header. It should be present | ||
**Arguments**: | ||
- **value** `string|regexp` - Header value to match. | ||
```js | ||
var rule = toxy.rules.contentType('application/json') | ||
toxy.rule(rule) | ||
``` | ||
#### Body | ||
`To do` | ||
### How to write rules | ||
Rules are simple middleware functions that resolve asyncronously with a `boolean` value to determine if a given HTTP transaction should be ignored when poisoning. | ||
Here's an example of a simple rule matching the HTTP method to determine if: | ||
```js | ||
function method(matchMethod) { | ||
/** | ||
* We name the function since it's used by toxy to identify the rule to get/disable/remove it in the future | ||
*/ | ||
return function method(req, res, next) { | ||
var shouldIgnore = req.method !== matchMethod | ||
next(shouldIgnore) | ||
} | ||
} | ||
// Register and enable the rule | ||
toxy | ||
.get('/foo') | ||
.rule(method('GET')) | ||
.poison(/* ... */) | ||
``` | ||
## Programmatic API | ||
`toxy` API is completely built on top the [rocky API](https://github.com/h2non/rocky#programmatic-api). In other words, you can use any of the methods, features and middleware layer natively provided by `rocky`. | ||
### toxy([ options ]) | ||
Create a new `toxy` proxy. | ||
For supported `options`, please see rocky [documentation](https://github.com/h2non/rocky#configuration) | ||
```js | ||
var toxy = require('toxy') | ||
toxy({ forward: 'http://server.net', timeout: 30000 }) | ||
toxy | ||
.get('/foo') | ||
.poison(toxy.poisons.latency(1000)) | ||
.withRule(toxy.rules.contentType('json')) | ||
.forward('http://foo.server') | ||
toxy | ||
.post('/bar') | ||
.poison(toxy.poisons.bandwidth({ bps: 1024 })) | ||
.withRule(toxy.rules.probability(50)) | ||
.forward('http://bar.server') | ||
toxy.all('/*') | ||
``` | ||
#### toxy#get(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#post(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#put(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#patch(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#delete(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#head(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#all(path, [ middleware... ]) | ||
Return: `ToxyRoute` | ||
#### toxy#forward(url) | ||
#### toxy#balance(urls) | ||
#### toxy#replay(url) | ||
#### toxy#use(middleware) | ||
#### toxy#useResponse(middleware) | ||
#### toxy#useReplay(middleware) | ||
#### toxy#poison(poison) | ||
Alias: `usePoison` | ||
#### toxy#rule(rule) | ||
Alias: `useRule` | ||
#### toxy#withRule(rule) | ||
Aliases: `poisonRule`, `poisonFilter` | ||
#### toxy#enable(poison) | ||
#### toxy#disable(poison) | ||
#### toxy#remove(poison) | ||
Return: `boolean` | ||
#### toxy#isEnabled(poison) | ||
Return: `boolean` | ||
#### toxy#disableAll() | ||
Alias: `disablePoisons` | ||
#### toxy#poisons() | ||
Return: `array<Directive>` Alias: `getPoisons` | ||
#### toxy#flush() | ||
Alias: `flushPoisons` | ||
#### toxy#enableRule(rule) | ||
#### toxy#disableRule(rule) | ||
#### toxy#removeRule(rule) | ||
Return: `boolean` | ||
#### toxy#disableRules() | ||
#### toxy#isRuleEnabled(rule) | ||
Return: `boolean` | ||
#### toxy#rules() | ||
Return: `array<Directive>` Alias: `getRules` | ||
#### toxy#flushRules() | ||
### ToxyRoute | ||
Toxy route has, indeed, the same interface as `Toxy` global interface, but further actions you perform againts the API will only be applicable at route-level. In other words: good news, you already know the API. | ||
This example will probably clarify possible doubts: | ||
```js | ||
var toxy = require('toxy') | ||
var proxy = toxy() | ||
// Now using the global API | ||
proxy | ||
.poison(toxy.poisons.bandwidth({ bps: 1024 })) | ||
.rule(toxy.rules.method('GET')) | ||
// Now create a route | ||
var route = proxy.get('/foo') | ||
// Now using the ToxyRoute interface | ||
route | ||
.poison(toxy.poisons.bandwidth({ bps: 512 })) | ||
.rule(toxy.rules.contentType('json')) | ||
``` | ||
### Directive(middlewareFn) | ||
A convenient wrapper internally used for poisons and rules. | ||
Normally you don't need to know this interface, but for hacking purposes or more low-level actions might be useful. | ||
#### Directive#enable() | ||
Return: `boolean` | ||
#### Directive#disable() | ||
Return: `boolean` | ||
#### Directive#isEnabled() | ||
Return: `boolean` | ||
#### Directive#rule(rule) | ||
Alias: `filter` | ||
#### Directive#handler() | ||
Return: `function(req, res, next)` | ||
## License | ||
MIT - Tomas Aparicio |
const http = require('http') | ||
const clone = require('clone') | ||
const expect = require('chai').expect | ||
@@ -13,6 +14,8 @@ const bandwidth = require('../..').poisons.bandwidth | ||
var lastWrite = Date.now() | ||
var origProto = res.__proto__ | ||
res = clone.clonePrototype(res) | ||
res.__proto__.write = function (buffer, encoding, next) { | ||
expect(buffer).to.have.length(opts.bps) | ||
expect(Date.now() - lastWrite).to.be.least(opts.threshold) | ||
expect(Date.now() - lastWrite).to.be.least(opts.threshold - 1) | ||
lastWrite = Date.now() | ||
@@ -26,2 +29,3 @@ buf.push(buffer) | ||
expect(buf.join('')).to.be.equal('Hello World') | ||
console.log('') | ||
done() | ||
@@ -28,0 +32,0 @@ } |
const sinon = require('sinon') | ||
const clone = require('clone') | ||
const expect = require('chai').expect | ||
@@ -12,6 +13,8 @@ const slowClose = require('../..').poisons.slowClose | ||
var res = {} | ||
res.writeHead = spy | ||
res.end = function (body) { | ||
var res = clone.clonePrototype({}) | ||
res.__proto__.writeHead = spy | ||
res.__proto__.end = function (body) { | ||
spy(body) | ||
res.__proto__ | ||
end() | ||
@@ -18,0 +21,0 @@ } |
const http = require('http') | ||
const sinon = require('sinon') | ||
const clone = require('clone') | ||
const expect = require('chai').expect | ||
@@ -14,2 +15,3 @@ const throttle = require('../..').poisons.throttle | ||
var lastWrite = Date.now() | ||
res = clone.clonePrototype(res) | ||
@@ -47,2 +49,3 @@ res.__proto__.write = function (buffer, encoding, next) { | ||
var lastWrite = Date.now() | ||
res = clone.clonePrototype(res) | ||
@@ -69,3 +72,2 @@ res.__proto__.write = function (buffer, encoding, next) { | ||
}) | ||
}) |
@@ -17,2 +17,14 @@ const expect = require('chai').expect | ||
test('partial string', function (done) { | ||
var type = 'json' | ||
var req = { headers: { 'content-type': 'application/json' } } | ||
contentType(type)(req, null, next) | ||
function next(ignore) { | ||
expect(ignore).to.be.false | ||
done() | ||
} | ||
}) | ||
test('regexp', function (done) { | ||
@@ -19,0 +31,0 @@ var type = /application\/json/i |
@@ -21,3 +21,3 @@ const expect = require('chai').expect | ||
method({ methods: ['POST', 'GET'] })(req, null, next) | ||
method(['POST', 'GET'])(req, null, next) | ||
@@ -24,0 +24,0 @@ function next(ignore) { |
103
test/toxy.js
@@ -0,5 +1,108 @@ | ||
const http = require('http') | ||
const expect = require('chai').expect | ||
const toxy = require('..') | ||
const supertest = require('supertest') | ||
suite('toxy', function () { | ||
test('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.VERSION).to.be.a('string') | ||
}) | ||
test('use poison', function (done) { | ||
var proxy = toxy() | ||
var called = false | ||
proxy.poison(function delay(req, res, next) { | ||
called = true | ||
setTimeout(next, 5) | ||
}) | ||
expect(proxy.isEnabled('delay')).to.be.true | ||
proxy.disable('delay') | ||
expect(proxy.isEnabled('delay')).to.be.false | ||
proxy.enable('delay') | ||
expect(proxy.isEnabled('delay')).to.be.true | ||
proxy._poisons.run(null, null, function () { | ||
expect(called).to.be.true | ||
done() | ||
}) | ||
}) | ||
test('use rule', function (done) { | ||
var proxy = toxy() | ||
var called = false | ||
proxy.rule(function delay(req, res, next) { | ||
called = true | ||
setTimeout(next, 5) | ||
}) | ||
expect(proxy.isRuleEnabled('delay')).to.be.true | ||
proxy.disableRule('delay') | ||
expect(proxy.isRuleEnabled('delay')).to.be.false | ||
proxy.enableRule('delay') | ||
expect(proxy.isRuleEnabled('delay')).to.be.true | ||
proxy._rules.run(null, null, function () { | ||
expect(called).to.be.true | ||
done() | ||
}) | ||
}) | ||
test('e2e', function (done) { | ||
var proxy = toxy() | ||
var server = createServer(9001, 200) | ||
var timeout = 100 | ||
proxy.poison(toxy.poisons.latency(timeout)) | ||
proxy.rule(function method(req, res, next) { | ||
next(req.method === 'GET' ? null : true) | ||
}) | ||
proxy.forward('http://localhost:9001') | ||
proxy.get('/foo') | ||
proxy.listen(9000) | ||
var init = Date.now() | ||
supertest('http://localhost:9000') | ||
.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) | ||
done(err) | ||
} | ||
}) | ||
}) | ||
function createServer(port, code, assert) { | ||
var server = http.createServer(function (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 | ||
end() | ||
}) | ||
function end() { | ||
if (assert) assert(req, res) | ||
res.end() | ||
} | ||
}) | ||
server.listen(port) | ||
return server | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
61595
60
1588
487
6
6