1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
|
A request/response rewriting HTTP proxy. A Rack app. Subclass `Rack::Proxy` and provide your `rewrite_env` and `rewrite_response` methods.
Installation
----
Add the following to your `Gemfile`:
```
gem 'rack-proxy', '~> 0.7.7'
```
Or install:
```
gem install rack-proxy
```
Use Cases
----
Below are some examples of real world use cases for Rack-Proxy. If you have done something interesting, add it to the list below and send a PR.
* Allowing one app to act as central trust authority
* handle accepting self-sign certificates for internal apps
* authentication / authorization prior to proxying requests to a blindly trusting backend
* avoiding CORs complications by proxying from same domain to another backend
* subdomain based pass-through to multiple apps
* Complex redirect rules
* redirect pages with different extensions (ex: `.php`) to another app
* useful for handling awkward redirection rules for moved pages
* fan Parallel Requests: turning a single API request to [multiple concurrent backend requests](https://github.com/typhoeus/typhoeus#making-parallel-requests) & merging results.
* inserting or stripping headers required or problematic for certain clients
Options
----
Options can be set when initializing the middleware or overriding a method.
* `:streaming` - defaults to `true`, but does not work on all Ruby versions, recommend to set to `false`
* `:ssl_verify_none` - tell `Net::HTTP` to not validate certs
* `:ssl_version` - tell `Net::HTTP` to set a specific `ssl_version`
* `:backend` - the URI parseable format of host and port of the target proxy backend. If not set it will assume the backend target is the same as the source.
* `:read_timeout` - set proxy timeout it defaults to 60 seconds
To pass in options, when you configure your middleware you can pass them in as an optional hash.
```ruby
Rails.application.config.middleware.use ExampleServiceProxy, backend: 'http://guides.rubyonrails.org', streaming: false
```
Examples
----
See and run the examples below from `lib/rack_proxy_examples/`. To mount any example into an existing Rails app:
1. create `config/initializers/proxy.rb`
2. modify the file to require the example file
```ruby
require 'rack_proxy_examples/forward_host'
```
### Forward request to Host and Insert Header
Test with `require 'rack_proxy_examples/forward_host'`
```ruby
class ForwardHost < Rack::Proxy
def rewrite_env(env)
env["HTTP_HOST"] = "example.com"
env
end
def rewrite_response(triplet)
status, headers, body = triplet
# example of inserting an additional header
headers["X-Foo"] = "Bar"
# if you rewrite env, it appears that content-length isn't calculated correctly
# resulting in only partial responses being sent to users
# you can remove it or recalculate it here
headers["content-length"] = nil
triplet
end
end
```
### Disable SSL session verification when proxying a server with e.g. self-signed SSL certs
Test with `require 'rack_proxy_examples/trusting_proxy'`
```ruby
class TrustingProxy < Rack::Proxy
def rewrite_env(env)
env["HTTP_HOST"] = "self-signed.badssl.com"
# We are going to trust the self-signed SSL
env["rack.ssl_verify_none"] = true
env
end
def rewrite_response(triplet)
status, headers, body = triplet
# if you rewrite env, it appears that content-length isn't calculated correctly
# resulting in only partial responses being sent to users
# you can remove it or recalculate it here
headers["content-length"] = nil
triplet
end
end
```
The same can be achieved for *all* requests going through the `Rack::Proxy` instance by using
```ruby
Rack::Proxy.new(ssl_verify_none: true)
```
### Rails middleware example
Test with `require 'rack_proxy_examples/example_service_proxy'`
```ruby
###
# This is an example of how to use Rack-Proxy in a Rails application.
#
# Setup:
# 1. rails new test_app
# 2. cd test_app
# 3. install Rack-Proxy in `Gemfile`
# a. `gem 'rack-proxy', '~> 0.7.7'`
# 4. install gem: `bundle install`
# 5. create `config/initializers/proxy.rb` adding this line `require 'rack_proxy_examples/example_service_proxy'`
# 6. run: `SERVICE_URL=http://guides.rubyonrails.org rails server`
# 7. open in browser: `http://localhost:3000/example_service`
#
###
ENV['SERVICE_URL'] ||= 'http://guides.rubyonrails.org'
class ExampleServiceProxy < Rack::Proxy
def perform_request(env)
request = Rack::Request.new(env)
# use rack proxy for anything hitting our host app at /example_service
if request.path =~ %r{^/example_service}
backend = URI(ENV['SERVICE_URL'])
# most backends required host set properly, but rack-proxy doesn't set this for you automatically
# even when a backend host is passed in via the options
env["HTTP_HOST"] = backend.host
# This is the only path that needs to be set currently on Rails 5 & greater
env['PATH_INFO'] = ENV['SERVICE_PATH'] || '/configuring.html'
# don't send your sites cookies to target service, unless it is a trusted internal service that can parse all your cookies
env['HTTP_COOKIE'] = ''
super(env)
else
@app.call(env)
end
end
end
```
### Using as middleware to forward only some extensions to another Application
Test with `require 'rack_proxy_examples/rack_php_proxy'`
Example: Proxying only requests that end with ".php" could be done like this:
```ruby
###
# Open http://localhost:3000/test.php to trigger proxy
###
class RackPhpProxy < Rack::Proxy
def perform_request(env)
request = Rack::Request.new(env)
if request.path =~ %r{\.php}
env["HTTP_HOST"] = ENV["HTTP_HOST"] ? URI(ENV["HTTP_HOST"]).host : "localhost"
ENV["PHP_PATH"] ||= '/manual/en/tutorial.firstpage.php'
# Rails 3 & 4
env["REQUEST_PATH"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
# Rails 5 and above
env['PATH_INFO'] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
env['content-length'] = nil
super(env)
else
@app.call(env)
end
end
def rewrite_response(triplet)
status, headers, body = triplet
# if you proxy depending on the backend, it appears that content-length isn't calculated correctly
# resulting in only partial responses being sent to users
# you can remove it or recalculate it here
headers["content-length"] = nil
triplet
end
end
```
To use the middleware, please consider the following:
1) For Rails we could add a configuration in `config/application.rb`
```ruby
config.middleware.use RackPhpProxy, {ssl_verify_none: true}
```
2) For Sinatra or any Rack-based application:
```ruby
class MyAwesomeSinatra < Sinatra::Base
use RackPhpProxy, {ssl_verify_none: true}
end
```
This will allow to run the other requests through the application and only proxy the requests that match the condition from the middleware.
See tests for more examples.
### SSL proxy for SpringBoot applications debugging
Whenever you need to debug communication with external services with HTTPS protocol (like OAuth based) you have to be able to access to your local web app through HTTPS protocol too. Typical way is to use nginx or Apache httpd as a reverse proxy but it might be inconvinuent for development environment. Simple proxy server is a better way in this case. The only what we need is to unpack incoming SSL queries and proxy them to a backend. We can prepare minimal set of files to create autonomous proxy server.
Create `config.ru` file:
```ruby
#
# config.ru
#
require 'rack'
require 'rack-proxy'
class ForwardHost < Rack::Proxy
def rewrite_env(env)
env['HTTP_X_FORWARDED_HOST'] = env['SERVER_NAME']
env['HTTP_X_FORWARDED_PROTO'] = env['rack.url_scheme']
env
end
end
run ForwardHost.new(backend: 'http://localhost:8080')
```
Create `Gemfile` file:
```ruby
source "https://rubygems.org"
gem 'thin'
gem 'rake'
gem 'rack-proxy'
```
Create `config.yml` file with configuration of web server `thin`:
```yml
---
ssl: true
ssl-key-file: keys/domain.key
ssl-cert-file: keys/domain.crt
ssl-disable-verify: false
```
Create 'keys' directory and generate SSL key and certificates files `domain.key` and `domain.crt`
Run `bundle exec thin start` for running it with `thin`'s default port.
Or use `sudo -E thin start -C config.yml -p 443` for running with default for `https://` port.
Don't forget to enable processing of `X-Forwarded-...` headers on your application side. Just add following strings to your `resources/application.yml` file.
```yml
---
server:
tomcat:
remote-ip-header: x-forwarded-for
protocol-header: x-forwarded-proto
use-forward-headers: true
```
Add some domain name like `debug.your_app.com` into your local `/etc/hosts` file like
```
127.0.0.1 debug.your_app.com
```
Next start the proxy and your app. And now you can access to your Spring application through SSL connection via `https://debug.your_app.com` URI in a browser.
### Using SSL/TLS certificates with HTTP connection
This may be helpful, when third-party API has authentication by client TLS certificates and you need to proxy your requests and sign them with certificate.
Just specify Rack::Proxy SSL options and your request will use TLS HTTP connection:
```ruby
# config.ru
. . .
cert_raw = File.read('./certs/rootCA.crt')
key_raw = File.read('./certs/key.pem')
cert = OpenSSL::X509::Certificate.new(cert_raw)
key = OpenSSL::PKey.read(key_raw)
use TLSProxy, cert: cert, key: key, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER, ssl_version: 'TLSv1_2'
```
And rewrite host for example:
```ruby
# tls_proxy.rb
class TLSProxy < Rack::Proxy
attr_accessor :original_request, :query_params
def rewrite_env(env)
env["HTTP_HOST"] = "client-tls-auth-api.com:443"
env
end
end
```
WARNING
----
Doesn't work with `fakeweb`/`webmock`. Both libraries monkey-patch net/http code.
Todos
----
* Make the docs up to date with the current use case for this code: everything except streaming which involved a rather ugly monkey patch and only worked in 1.8, but does not work now.
* Improve and validate requirements for Host and Path rewrite rules
* Ability to inject logger and set log level
|