rocky
Pluggable, full featured and middleware-oriented HTTP/S proxy with versatile routing layer, traffic interceptor and replay to multiple backends, built-in balancer, hierarchical configuration and more.
Built for node.js/io.js.
Compatible with connect/express.
rocky
can be fluently used programmatically or via command-line interface.
To get started, take a look to the how does it work, basic usage, examples and third-party middleware
Requires node.js +0.12 or io.js +1.6
Contents
Features
- Full-featured HTTP/S proxy (backed by http-proxy)
- Replay traffic to multiple backends
- Able to run as standalone HTTP/S server
- Easily integrable with connect/express via middleware
- Full-featured built-in router with regexp and params matching
- Hierarchial router configuration
- Hierarchial middleware layer (supports multiple hooks)
- Able to capture traffic as interceptor pattern
- Built-in traffic sniffer and transformer for request/response payloads
- Built-in load balancer
- Hierarchical configuration
- Compatible with most of the existing connect/express middleware
- Fluent, elegant and evented programmatic API
- Simple command-line interface with declarative configuration file
When rocky
could be useful?
- As HTTP proxy for progressive migrations (e.g: APIs)
- As HTTP traffic interceptor transforming the request/response on-the-fly
- As intermediate HTTP proxy adapter for external services
- Replaying traffic to one or multiple backend
- As standalone reverse HTTP proxy with powerful routing
- As security proxy layer with custom logic
- As extensible HTTP proxy balancer with custom logic per specific route
- As HTTP load balancer with zero-downtime
- As HTTP API gateway
- As SSL terminator proxy
- For A/B testing
- As test intermediate servercd intercepting and generating random/fake responses
- And whatever a programmatic HTTP proxy could be useful to
Installation
npm install rocky --save
For command-line interface usage, install it as global package:
npm install -g rocky
Standalone binaries
Packaged using nar. Shipped with node.js 0.12.6
Usage
chmod +x rocky-0.2.0-linux-x64.nar
./rocky-0.2.0-linux-x64.nar exec --port 3000 --config rocky.toml
Introduction
Motivation
Migrating systems if not a trivial thing, and it's even more complex if we're talking about production systems that require high availability. Taking care of consistency and public interface contract should be a premise in most cases.
rocky
was initially created to become an useful tool for assisting during a backend migration strategy. However, it could be useful for many other scenarios.
Design
rocky
design is driven by keeping versatility and extensibility in mind. The main goal is to keep it with a small core and codebase with just the proper responsability and built-in features, but highly opened to extensibility.
Middleware layer is probably the core and more useful feature of rocky
.
The significant difference betweet the middleware layer and an event bus, which is very common in asynchronous programming, is the control flow capability. Via middleware you can completely rely on a consistent control flow when handling some data, continuing or stoping it accordingly.
This approach allow you to plug in intermedia jobs with custom logic beetwen stages of the HTTP traffic flow live cycle.
Stability
rocky is relative young but production focused package.
Version 0.1.x
was wrote during my free time in less than 10 days (mostly at night during the weekend), therefore it could be considered in beta
stage.
Version 0.2.x
introduced significant improvements such as a more consistent API and a new hierarchical middleware layer.
This version is focused on stability and production use, however it's only recommended to use it in non-hostile environments for now.
Versions
- 0.1.x
beta
- First version. Initially released at 25.06.2015
. - 0.2.x
beta
- Released at 07.07.2015
.
How does it work?
rocky
could be useful in multiple scenarios, but a common and representative use case scenario could be the following:
|==============|
| The Internet |
|==============|
||||
|==============|
| HTTP proxy |
|~~~~~~~~~~~~~~|
| Rocky Router |
|~~~~~~~~~~~~~~|
| Middleware |
|==============|
|| |
(duplex) // \ (one-way)
// \
// \
/----------\ /----------\ /----------\
| target | | replay 1 | -> | replay 2 | (*N)
\----------/ \----------/ \----------/
Middleware layer
rocky
provides a build-in featured and powerful connect-style middleware that allow
you to augment its functionality easily.
The middleware layer is compatible with
Hierarchies
rocky
supports multiple middleware hierarchies:
- global - Dispached on every incoming request matched by the router
- route - Dispached only per route scope
Types of middleware
rocky
introduces multiple types of middleware layers based on the same interface and behavior of connect/express middleware.
This was introduced in order to achieve in a more responsive way multiple traffic flows in the scope of a HTTP proxy.
Those flows are intrinsicly correlated but might be handled in a completely different way.
The goal is to allowing you to handle them acordingly, acting in the middle of those phases to augment some functionality or react to some event with better precisision.
Supported types of middleware:
router
- Scope:
global
- Description: Dispatched on every matched route.
- Notation:
.use([path], function (req, res, next))
forward
- Scope:
global
, route
- Description: Dispached before forwarding an incoming request.
- Notation:
.useForward(function (req, res, next))
replay
- Scope:
global
, route
- Description: Dispached before starting each replay request.
- Notation:
.useReplay(function (req, res, next))
param
- Scope:
global
- Description: Dispached on every matched param on any route.
- Notation:
.useParam(function (req, res, next))
Middleware flow
The following diagram explains the request flow and how the different middleware layers are involved in it:
↓ ( Incoming request ) ↓
↓ ||| ↓
↓ ---------------- ↓
↓ | Router | ↓ --> Match a route, dispatching its middleware if required
↓ ---------------- ↓
↓ ||| ↓
↓ --------------------- ↓
↓ | Global middleware | ↓ --> Dispatch on every incoming request (Global)
↓ --------------------- ↓
↓ ||| ↓
↓ / \ ↓
↓ / \ ↓
↓ / \ ↓
↓ [ Forward ] [ Replay ] ↓ --> Dispatch both middleware in separated flows (Global, Route)
↓ \ / ↓
↓ \ / ↓
↓ \ / ↓
↓ ------------------ ↓
↓ | HTTP dispacher | ↓ --> Send requests over the network, separately
↓ ------------------ ↓
Middleware API
Middleware behavior and interface are the same like connect/express,
so you can create middleware as you already know with the notation function(req, res, next)
rocky
exposes as a sort of inversion of control in every http.ClientRequest
object the following fields:
- req.rocky
object
- .options
object
- Expose the configuration options for the current request. - .proxy
Rocky
- Expose the rocky instance. Use only for hacking purposes! - .route
Route
- Expose the current running route. Only available in route
type middleware
This provides you way to extend or modify specific values from the middleware layer without having side-effects,
for instance replacing the server target URL, like in the following example:
rocky()
.get('/users/:name')
.forward('http://old.server.net')
.use(function (req, res, next) {
if (req.param.name === 'admin') {
req.rocky.options.target = 'http://new.server.net'
}
next()
})
Third-party middleware
- consul - Dynamic service discovery and balancing using Consul
- vhost - vhost based proxy routing for rocky
- version - HTTP API version based routing (uses http-version)
Note that you can use any other existent middleware plug in rocky
as part of your connect/express app.
Additionally, rocky
provides some built-in middleware functions that you can plug in different types of middleware.
Command-line
Start rocky HTTP proxy server
Usage: rocky [options]
Options:
--help, -h Show help [boolean]
--config, -c File path to TOML config file
--port, -p rocky HTTP server port
--forward, -f Default forward server URL
--replay, -r Define a replay server URL
--key, -k Path to SSL key file
--cert, -e Path to SSL certificate file
--secure, -s Enable SSL certification validation
--balance, -b Define server URLs to balance between, separated by commas
--debug, -d Enable debug mode [boolean]
-v, --version Show version number [boolean]
Examples:
rocky -c rocky.toml \
-f http://127.0.0.1:9000 \
-r http://127.0.0.1
Examples
Passing the config file:
rocky --config rocky.toml --port 8080 --debug
Reading config from stdin
:
cat rocky.toml | rocky --port 8080 --debug
Transparent rocky.toml
file discovery in current and higher directories:
rocky --port 8080
Configuration
Supported params
- forward
string
- Default forward URL - debug
boolean
- Enable debug mode. Default false
- target
string
- <url string to be parsed with the url module - replay
array<string>
- Optional replay server URLs. Via API you should use the replay()
method - balance
array<url>
- Define the URLs to balance. Via API you should use the balance()
method - forward
string
- url string to be parsed with the url module - timeout
number
- Timeout for request socket - proxyTimeout
number
- Timeout for proxy request socket - agent
object
- object to be passed to http(s).request. See node.js https
docs - ssl
object
- object to be passed to https.createServer()
- cert
string
- Path to SSL certificate file - key
string
- Path to SSL key file
- ws
boolean
- true/false, if you want to proxy websockets - xfwd
boolean
- true/false, adds x-forward headers - secure
boolean
- true/false, verify SSL certificate - toProxy
boolean
- true/false, explicitly specify if we are proxying to another proxy - prependPath
boolean
- true/false, Default: true - specify whether you want to prepend the target's path to the proxy path - ignorePath
boolean
- true/false, Default: false - specify whether you want to ignore the proxy path of the incoming request - localAddress
boolean
- <Local interface string to bind for outgoing connections - changeOrigin
boolean
- <true/false, Default: false - changes the origin of the host header to the target URL - auth
boolean
- Basic authentication i.e. 'user:password' to compute an Authorization header. - hostRewrite
boolean
- rewrites the location hostname on (301/302/307/308) redirects, Default: null. - autoRewrite
boolean
- rewrites the location host/port on (301/302/307/308) redirects based on requested host/port. Default: false. - protocolRewrite
boolean
- rewrites the location protocol on (301/302/307/308) redirects to 'http' or 'https'. Default: null. - forwardOriginalBody
boolean
- Only valid for replay request. Forward the original body instead of the transformed one - router
object
- Specific router params
- strict
boolean
- When false
trailing slashes are optional (default: false
) - caseSensitive
boolean
- When true
the routing will be case sensitive. (default: false
) - mergeParams
boolean
- When true
any req.params
passed to the router will be
merged into the router's req.params
. (default: false
)
Configuration file
Default configuration file name: rocky.toml
The configuration file must be declared in TOML language
port = 8080
forward = "http://google.com"
replay = ["http://duckduckgo.com"]
[ssl]
cert = "server.crt"
key = "server.key"
[/users/:id]
method = "all"
forward = "http://new.server"
[/oauth]
method = "all"
forward = "http://auth.server"
[/*]
method = "GET"
forward = "http://old.server"
[/download/:file]
method = "GET"
timeout = 5000
balance = ["http://1.file.server", "http://2.file.server"]
[/photo/:name]
method = "GET"
[[replay]]
target = "http://old.server"
forwardHost = true
[[replay]]
target = "http://backup.server"
Programmatic API
Usage
Example using Express
var rocky = require('rocky')
var express = require('express')
var app = express()
var proxy = rocky()
proxy
.forward('http://new.server')
.replay('http://old.server')
.replay('http://log.server')
.options({ forwardHost: true })
proxy
.get('/users/:id')
proxy
.get('/download/:file')
.balance(['http://1.file.server', 'http://2.file.server'])
app.use(proxy.middleware())
app.get('/users/:id', function () { })
app.listen(3000)
Example using the built-in HTTP server
var rocky = require('rocky')
var proxy = rocky()
proxy
.forward('http://new.server')
.replay('http://old.server', { forwardOriginalBody: true })
.options({ forwardHost: true })
.on('proxy:error', function (err) {
console.error('Error:', err)
})
.on('proxyReq', function (proxyReq, req, res, opts) {
console.log('Proxy request:', req.url, 'to', opts.target)
})
.on('proxyRes', function (proxyRes, req, res) {
console.log('Proxy response:', req.url, 'with status', res.statusCode)
})
proxy
.get('/users/:id')
.toPath('/profile/:id')
.headers({
'Authorization': 'Bearer 0123456789'
})
proxy
.get('/search')
.forward('http://another.server')
.use(function (req, res, next) {
if (req.headers['Autorization'] !== 'Bearer 012345678') {
res.statusCode = 401
return res.end()
}
next()
})
.transformResponseBody(function (req, res, next) {
var body = JSON.parse(res.body.toString())
var newBody = JSON.stringify({ salutation: 'hello ' + body.hello })
next(null, newBody)
})
proxy.listen(3000)
For more usage cases, take a look at the examples
rocky([ options ])
Creates a new rocky instance with the given options.
You can pass any of the allowed params at configuration level and any supported http-proxy options
rocky#forward(url)
Alias: target
Define a default target URL to forward the request
rocky#replay(url, [ opts ])
Add a server URL to replay the incoming request
opts
param provide specific replay options, overwritting the parent options.
rocky#options(options)
Define/overwrite rocky server options.
You can pass any of the supported options by http-proxy
.
rocky#use([ path ], ...middleware)
Use the given middleware to handle all http methods on the given path, defaulting to the root path.
rocky#useParam(param, ...middleware)
Alias: param()
Maps the specified path parameter name to a specialized param-capturing middleware.
The middleware stack is the same as .use()
.
rocky#useReplay(...middleware)
Use a given middleware to handle the replay traffic.
rocky#useFor(name, ...middleware)
Use a custom middleware for a specific phase. Supported phase names are: forward
, 'replay'.
This method is used internally, but it's also public since it could be useful for dynamic programmatic middleware configuration, instead of using the shortcut methods such as: useReplay
or useForward
.
rocky#balance(...urls)
Define a set of URLs to balance between with a simple round-robin like scheduler.
rocky#on(event, handler)
Subscribe to a proxy event.
See support events here
rocky#once(event, handler)
Remove an event by its handler function.
See support events here
rocky#off(event, handler)
Remove an event by its handler function.
See support events here
rocky#removeAllListeners(event)
Remove all the subscribers to the given event.
See support events here
rocky#middleware()
Return: Function(req, res, next)
Return a connect/express compatible middleware
rocky#requestHandler(req, res, next)
Raw HTTP request/response handler.
rocky#listen(port, [ host ])
Starts a HTTP proxy server in the given port
rocky#close([ callback ])
Close the HTTP proxy server, if exists.
Shortcut to rocky#server.close(cb)
rocky#all(path, [ ...middleware ])
Return: Route
Add a route handler for the given path for all HTTP methods
rocky#get(path, [ ...middleware ])
Return: Route
Configure a new route the given path with GET
method
rocky#post(path, [ ...middleware ])
Return: Route
Configure a new route the given path with POST
method
rocky#put(path, [ ...middleware ])
Return: Route
Configure a new route the given path with PUT
method
rocky#delete(path, [ ...middleware ])
Return: Route
Configure a new route the given path with DELETE
method
rocky#patch(path, [ ...middleware ])
Return: Route
Configure a new route the given path with PATCH
method
rocky#head(path, [ ...middleware ])
Return: Route
Configure a new route the given path with HEAD
method
rocky#router
Internal router instance
rocky#server
HTTP/HTTPS server instance.
Only present if listen()
was called starting the built-in server.
Route(path)
route#forward(url)
Alias: target
Overwrite forward server for the current route.
route#replay(url, [ opts ])
Overwrite replay servers for the current route.
opts
param provide specific replay options, overwritting the parent options.
route#balance(urls)
Define a set of URLs to balance between with a simple round-robin like scheduler.
urls
param must be an array of strings.
route#reply(status, [ headers, body ])
Shortcut method to intercept and reply the incoming request.
If used, body
param must be a string
or buffer
route#toPath(url, [ params ])
Overwrite the request path, defining additional optional params.
Define or overwrite request headers for the current route.
route#host(host)
Overwrite the Host
header value when forward the request.
route#redirect(url)
Redirect the incoming request for the current route.
route#transformRequestBody(middleware, [ filter ])
This method implements a non-instrusive native http.IncommingMessage
stream wrapper that allow you to intercept and transform the request body received from the client before sending it to the target server.
The middleware
argument must a function which accepts the following arguments: function(req, res, next)
The filter
arguments is optional and it can be a string
, regexp
or function(req)
which should return boolean
if the request
passes the filter. The default check value by string
or regexp
test is the Content-Type
header.
In the middleware function must call the next
function, which accepts the following arguments: err, newBody, encoding
You can see an usage example here.
Caution: using this middleware could generate in some scenarios negative performance side-effects, since the whole payload data will be buffered in the heap until it's finished. Don't use it if you need to handle large payloads.
The body will be exposed as raw Buffer
or String
on both properties body
and originalBody
in http.ClientRequest
:
rocky
.post('/users')
.transformRequestBody(function (req, res, next) {
var body = JSON.parse(req.body.toString())
var newBody = JSON.stringify({ salutation: 'hello ' + body.hello })
next(null, newBody, 'utf8')
}, function (req) {
return /application\/json/i.test(req.headers['content-type'])
})
route#transformResponseBody(middleware, [ filter ])
This method implements a non-instrusive native http.RequestResponse
stream wrapper that allow you to intercept and transform the response body received from the target server before sending it to the client.
The middleware
argument must a function which accepts the following arguments: function(req, res, next)
The filter
arguments is optional and it can be a string
, regexp
or function(res)
which should return boolean
if the request
passes the filter. The default check value by string
or regexp
test is the Content-Type
header.
In the middleware function must call the next
function, which accepts the following arguments: err, newBody, encoding
You can see an usage example here.
Caution: using this middleware could generate in some scenarios negative performance side-effects since the whole payload data will be buffered in the heap until it's finished. Don't use it if you need to handle large payloads.
The body will be exposed as raw Buffer
or String
on both properties body
and originalBody
in http.ClientResponse
:
rocky
.post('/users')
.transformResponseBody(function (req, res, next) {
var body = JSON.parse(res.body.toString())
var newBody = JSON.stringify({ salutation: 'hello ' + body.hello })
next(null, newBody, 'utf8')
}, function (res) {
return /application\/json/i.test(res.getHeader('content-type'))
})
route#options(options)
Overwrite default proxy options for the current route.
You can pass any supported option by http-proxy
route#use(...middleware)
Add custom middleware to the specific route.
rocky#useFor(name, ...middleware)
rocky#useReplay(...middleware)
rocky#useForward(...middleware)
route#on(event, ...handler)
Subscribes to a specific event for the given route.
Useful to incercept the status or modify the options on-the-fly
Events
- proxyReq
opts, proxyReq, req, res
- Fired when the request forward starts - proxyRes
opts, proxyRes, req, res
- Fired when the target server respond - proxy:error
err, req, res
- Fired when the proxy request fails - route:error
err, req, res
- Fired when cannot forward/replay the request or middleware error - replay:start
params, opts, req
- Fired before a replay request starts - replay:error
opts, err, req, res
- Fired when the replay request fails - server:error
err, req, res
- Fired on server middleware error. Only available if running as standalone HTTP server - route:missing
req, res
- Fired on missing route. Only available if running as standalone HTTP server
For more information about events, see the events fired by http-proxy
route#once(event, ...handler)
Subscribes to a specific event for the given route, and unsubscribes after dispatched
route#off(event, handler)
Remove an event by its handler function in the current route
rocky.create(config)
Create a standalone rocky
server with the given config
options.
See the supported config fields
var config = {
'forward': 'http://google.com',
'/search': {
method: 'GET',
forward: 'http://duckduckgo.com'
replay: ['http://bing.com', 'http://yahoo.com']
},
'/users/:id': {
method: 'all'
},
'/*': {
method: 'all',
forward: 'http://bing.com'
}
}
rocky.create(config)
rocky.middleware
Expose the built-in internal middleware functions.
You can reuse them as standard middleware in diferent ways, like this:
rocky()
.all('/*')
.use(rocky.middleware.headers({
'Authorization': 'Bearer 0123456789'
}))
.useReplay(rocky.middleware.host('replay.server.net'))
rocky.middleware.requestBody(middleware)
Intercept and optionally transform/replace the request body before forward it to the target server.
See rocky#transformRequestBody for more details.
rocky.middleware.responseBody(middleware)
Intercept and optionally transform/replace the response body from the server before send it to the client.
See rocky#transformResponseBody for more details.
rocky.middleware.toPath(path, [ params ])
Overrites the request URL path of the incoming request before forward/replay it.
Add/extend custom headers to the incoming request before forward/replay it.
rocky.middleware.host(host)
Overwrite the Host
header before forwarding/replaying the request. Useful for some scenarios (e.g Heroku).
rocky.middleware.reply(status, [ headers, body ])
Shortcut method to reply the intercepted request from the middleware, with optional headers
and body
data.
rocky.middleware.redirect(url)
Shortcut method to redirect the current request.
rocky.httpProxy
Accessor for the http-proxy API
rocky.VERSION
Current rocky package semver
Special Thanks
- http-proxy package creators and maintainers
- router package creators and maintainers
License
MIT - Tomas Aparicio