Socket
Socket
Sign inDemoInstall

toxy

Package Overview
Dependencies
30
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.2.2 to 0.3.0

examples/poison-phases.js

4

examples/admin.js

@@ -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() {

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",

@@ -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()
}
})
})

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc