rack-proxy 0.7.7 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,79 +1,182 @@
1
- A request/response rewriting HTTP proxy. A Rack app. Subclass `Rack::Proxy` and provide your `rewrite_env` and `rewrite_response` methods.
1
+ # Rack::Proxy
2
2
 
3
- Installation
4
- ----
3
+ [![Gem Version](https://img.shields.io/gem/v/rack-proxy)](https://rubygems.org/gems/rack-proxy)
4
+ [![CI](https://github.com/ncr/rack-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/ncr/rack-proxy/actions/workflows/ci.yml)
5
+ [![Downloads](https://img.shields.io/gem/dt/rack-proxy)](https://rubygems.org/gems/rack-proxy)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
7
 
6
- Add the following to your `Gemfile`:
8
+ A request/response rewriting HTTP proxy for Rack. Run it standalone as a tiny reverse proxy, or mount it as middleware and subclass it to rewrite requests and responses in flight.
7
9
 
10
+ - **Streams by default** — response bodies are relayed chunk by chunk straight off the backend socket, so large responses never buffer in memory.
11
+ - **Safe by default** — TLS verification on (`VERIFY_PEER`), Host-derived backends refused unless explicitly opted in, hop-by-hop headers stripped in both directions, backend failures mapped to `502` instead of raising.
12
+ - **Small** — two files on top of plain `Net::HTTP`; the only runtime dependency is Rack.
13
+
14
+ Typical uses:
15
+
16
+ - an authenticating/authorizing gateway in front of a trusting internal backend
17
+ - serving another app from the same origin to avoid CORS complications
18
+ - subdomain- or path-based routing to multiple internal services
19
+ - redirecting awkward legacy paths (e.g. `.php` pages) to another app
20
+ - inserting or stripping headers that are required — or problematic — for certain clients
21
+
22
+ ## Contents
23
+
24
+ - [Installation](#installation)
25
+ - [Quick start](#quick-start)
26
+ - [How it works](#how-it-works)
27
+ - [Options](#options)
28
+ - [Security considerations](#security-considerations)
29
+ - [Recipes](#recipes)
30
+ - [Upgrading](#upgrading)
31
+ - [Header keys and underscores](#header-keys-and-underscores)
32
+ - [Compatibility notes](#compatibility-notes)
33
+ - [Development](#development)
34
+
35
+ ## Installation
36
+
37
+ Requires Ruby >= 3.0 and Rack 2.x or 3.x. Add to your `Gemfile`:
38
+
39
+ ```ruby
40
+ gem "rack-proxy", "~> 1.0"
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ A standalone reverse proxy is one line of `config.ru`:
46
+
47
+ ```ruby
48
+ require "rack-proxy"
49
+
50
+ run Rack::Proxy.new(backend: "http://localhost:8080")
8
51
  ```
9
- gem 'rack-proxy', '~> 0.7.7'
52
+
53
+ As middleware, subclass it and decide per request: call `super` to proxy, or hand the request to the rest of your app:
54
+
55
+ ```ruby
56
+ class ApiProxy < Rack::Proxy
57
+ def perform_request(env)
58
+ if env["PATH_INFO"].start_with?("/api/")
59
+ env["HTTP_HOST"] = "api.internal.example" # most backends route on Host
60
+ super
61
+ else
62
+ @app.call(env)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Rails (config/initializers/proxy.rb):
68
+ Rails.application.config.middleware.use ApiProxy, backend: "https://api.internal.example"
69
+
70
+ # Any Rack app (config.ru or Sinatra):
71
+ use ApiProxy, backend: "https://api.internal.example"
10
72
  ```
11
73
 
12
- Or install:
74
+ ## How it works
75
+
76
+ Every request runs through a three-step pipeline:
13
77
 
14
78
  ```
15
- gem install rack-proxy
79
+ call(env) rewrite_env(env) → perform_request(env) → rewrite_response([status, headers, body])
16
80
  ```
17
81
 
18
- Use Cases
19
- ----
82
+ Override the steps you need:
20
83
 
21
- 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.
84
+ - **`rewrite_env(env)`** modify the request before it is forwarded (`HTTP_HOST`, path, headers, …). Return the env.
85
+ - **`rewrite_response(triplet)`** — post-process the backend's `[status, headers, body]`. Return the triplet. If you change the body, delete or recalculate `Content-Length` (`headers["content-length"] = nil`) or clients may receive truncated responses.
86
+ - **`perform_request(env)`** — take over routing: `super` proxies the request, `@app.call(env)` passes it through to the wrapped app (middleware mode).
87
+ - **`backend_allowed?(backend)`** — per-request allowlist hook, consulted for every request; return `false` to refuse with `502`. See [Security considerations](#security-considerations).
22
88
 
23
- * Allowing one app to act as central trust authority
24
- * handle accepting self-sign certificates for internal apps
25
- * authentication / authorization prior to proxying requests to a blindly trusting backend
26
- * avoiding CORs complications by proxying from same domain to another backend
27
- * subdomain based pass-through to multiple apps
28
- * Complex redirect rules
29
- * redirect pages with different extensions (ex: `.php`) to another app
30
- * useful for handling awkward redirection rules for moved pages
31
- * fan Parallel Requests: turning a single API request to [multiple concurrent backend requests](https://github.com/typhoeus/typhoeus#making-parallel-requests) & merging results.
32
- * inserting or stripping headers required or problematic for certain clients
89
+ Two request-scoped overrides can also be set in `env` (e.g. from `rewrite_env`):
33
90
 
34
- Options
35
- ----
91
+ - `env["rack.backend"]` — a URI (or URI-parseable string) overriding `:backend` for this request.
92
+ - `env["http.read_timeout"]` — override `:read_timeout` for this request.
36
93
 
37
- Options can be set when initializing the middleware or overriding a method.
94
+ ## Options
38
95
 
96
+ Pass options when instantiating (`Rack::Proxy.new(backend: ...)`) or mounting middleware (`use ApiProxy, backend: ...`).
39
97
 
40
- * `:streaming` - defaults to `true`, but does not work on all Ruby versions, recommend to set to `false`
41
- * `:ssl_verify_none` - tell `Net::HTTP` to not validate certs
42
- * `:ssl_version` - tell `Net::HTTP` to set a specific `ssl_version`
43
- * `: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.
44
- * `:read_timeout` - set proxy timeout it defaults to 60 seconds
98
+ ### Routing and mode
45
99
 
46
- To pass in options, when you configure your middleware you can pass them in as an optional hash.
100
+ - `:backend` URI (or URI-parseable string) of the backend host/port/scheme to proxy to. If not set, the destination is derived from the incoming request's `Host` which is **refused by default** since 1.0; see `:allow_dynamic_backend`.
101
+ - `:allow_dynamic_backend` — opt in (`true`) to deriving the destination from the client-supplied `Host` header when no `:backend` is configured. Off by default (such requests get `502`), because a bare dynamic proxy is an open proxy. Combine with a `backend_allowed?` allowlist.
102
+ - `:streaming` — stream the backend response as it arrives (default `true`). Set to `false` to buffer the whole response before returning it (also recommended under `webmock`/`vcr` — see [Compatibility notes](#compatibility-notes)).
47
103
 
48
- ```ruby
49
- Rails.application.config.middleware.use ExampleServiceProxy, backend: 'http://guides.rubyonrails.org', streaming: false
50
- ```
104
+ ### TLS
51
105
 
52
- Examples
53
- ----
106
+ - `:ssl_verify_none` — skip TLS certificate verification. Verification is on by default (`VERIFY_PEER`) — see [Upgrading](#upgrading).
107
+ - `:verify_mode` — explicit `OpenSSL::SSL::VERIFY_*` constant; wins over `ssl_verify_none`.
108
+ - `:ca_file` — path to a PEM CA bundle used to verify the backend certificate (prefer this over disabling verification for private CAs).
109
+ - `:cert_store` — an `OpenSSL::X509::Store` used to verify the backend certificate.
110
+ - `:cert` / `:key` — client certificate and key for mutual TLS to the backend.
111
+ - `:min_version` / `:max_version` — TLS protocol range (e.g. `:TLS1_2`), mapped to `Net::HTTP#min_version=` / `#max_version=`.
112
+ - `:ssl_version` — **deprecated**; pins an exact protocol (forbids TLS 1.3). Use `:min_version` / `:max_version`.
54
113
 
55
- See and run the examples below from `lib/rack_proxy_examples/`. To mount any example into an existing Rails app:
114
+ ### Timeouts and limits
56
115
 
57
- 1. create `config/initializers/proxy.rb`
58
- 2. modify the file to require the example file
59
- ```ruby
60
- require 'rack_proxy_examples/forward_host'
61
- ```
116
+ - `:read_timeout` — per-read timeout in seconds (default `60`).
117
+ - `:open_timeout` connection-open timeout in seconds.
118
+ - `:write_timeout` — per-write timeout in seconds.
119
+ - `:max_response_length` — cap (in bytes) on the backend response size; a larger response is refused with `502` (streaming aborts once the cap is passed).
120
+
121
+ ### Request shaping
122
+
123
+ - `:username` / `:password` — HTTP Basic credentials sent to the backend.
124
+ - `:strip_credentials` — when `true`, drop the client's `Cookie` and `Authorization` headers instead of forwarding them — see [Security considerations](#security-considerations). The strip applies **after** `rewrite_env`, so a credential injected there is stripped too; attach a proxy-owned credential with `:username`/`:password` instead.
125
+ - `:replace_x_forwarded_for` — when `true`, discard the client-supplied `X-Forwarded-For` chain and forward only this hop's `REMOTE_ADDR` (default appends to the chain) — see [Security considerations](#security-considerations).
126
+
127
+ ### Debugging
128
+
129
+ - `:logger` — any object responding to `#<<` (e.g. `$stdout`, a `StringIO`, or a Ruby `Logger`). Wired to `Net::HTTP#set_debug_output` so the HTTP wire-level conversation is written to the sink.
130
+
131
+ ## Security considerations
132
+
133
+ rack-proxy forwards attacker-influenced requests to a backend and relays the backend's response. Configure and subclass it with that in mind.
134
+
135
+ - **SSRF / open proxy — safe by default since 1.0.** If you do **not** set `:backend`, the destination host/port/scheme would be derived from the incoming request's `Host` / `X-Forwarded-Host` header — meaning a client could steer the proxy at *any* host, including cloud metadata endpoints (`169.254.169.254`), loopback, and private ranges. Such requests are now refused with `502` unless you pass `allow_dynamic_backend: true`. When you do opt in, pin an allowlist on top by overriding `backend_allowed?(backend)` (consulted for every request, static backends included):
136
+
137
+ ```ruby
138
+ class MyProxy < Rack::Proxy
139
+ ALLOWED = %w[api.internal.example.com].freeze
140
+
141
+ def backend_allowed?(backend)
142
+ ALLOWED.include?(backend.host)
143
+ end
144
+ end
145
+
146
+ MyProxy.new(allow_dynamic_backend: true)
147
+ ```
148
+
149
+ A refused backend is answered with `502` (with a hint in the `:logger` output).
62
150
 
63
- ### Forward request to Host and Insert Header
151
+ - **Credential forwarding.** All incoming `HTTP_*` headers are forwarded, including `Authorization` and `Cookie`. Don't proxy to a different trust domain with credentials attached — pass `strip_credentials: true` to drop both (or do finer-grained filtering in `rewrite_env`). Over an `http://` backend these travel in cleartext.
64
152
 
65
- Test with `require 'rack_proxy_examples/forward_host'`
153
+ - **X-Forwarded-For.** rack-proxy appends `REMOTE_ADDR` to any inbound `X-Forwarded-For`. If your clients are not behind a trusted proxy, the inbound value is attacker-controlled; pass `replace_x_forwarded_for: true` to forward only the directly-connected peer's address when the backend trusts that header.
154
+
155
+ - **TLS verification** defaults to `VERIFY_PEER`. For private-CA backends use `:ca_file` / `:cert_store` rather than `ssl_verify_none: true`.
156
+
157
+ - **Resource limits.** Use `:max_response_length` plus `:open_timeout` / `:write_timeout` / `:read_timeout` to bound memory and stalls against a hostile or slow backend.
158
+
159
+ - **Hop-by-hop headers** (Connection, TE, Transfer-Encoding, Proxy-Authorization, …) are stripped from both the forwarded request and the response.
160
+
161
+ To report a vulnerability, see [SECURITY.md](SECURITY.md).
162
+
163
+ ## Recipes
164
+
165
+ The snippets below (also in [`examples/`](examples/) in the repository) are meant to be **copied into your app** — e.g. into `app/middleware/` or `lib/` — and adapted. They are not shipped in the gem and cannot be `require`d from it.
166
+
167
+ ### Rewrite the Host and add a response header
168
+
169
+ From [`examples/forward_host.rb`](examples/forward_host.rb):
66
170
 
67
171
  ```ruby
68
172
  class ForwardHost < Rack::Proxy
69
-
70
173
  def rewrite_env(env)
71
174
  env["HTTP_HOST"] = "example.com"
72
175
  env
73
176
  end
74
177
 
75
178
  def rewrite_response(triplet)
76
- status, headers, body = triplet
179
+ _, headers, _ = triplet
77
180
 
78
181
  # example of inserting an additional header
79
182
  headers["X-Foo"] = "Bar"
@@ -85,27 +188,27 @@ class ForwardHost < Rack::Proxy
85
188
 
86
189
  triplet
87
190
  end
88
-
89
191
  end
90
192
  ```
91
193
 
92
- ### Disable SSL session verification when proxying a server with e.g. self-signed SSL certs
194
+ ```ruby
195
+ # config/initializers/proxy.rb
196
+ Rails.application.config.middleware.use ForwardHost, backend: "http://example.com"
197
+ ```
198
+
199
+ ### Proxy to a backend with a self-signed certificate
93
200
 
94
- Test with `require 'rack_proxy_examples/trusting_proxy'`
201
+ From [`examples/trusting_proxy.rb`](examples/trusting_proxy.rb):
95
202
 
96
203
  ```ruby
97
204
  class TrustingProxy < Rack::Proxy
98
-
99
205
  def rewrite_env(env)
100
206
  env["HTTP_HOST"] = "self-signed.badssl.com"
101
-
102
- # We are going to trust the self-signed SSL
103
- env["rack.ssl_verify_none"] = true
104
207
  env
105
208
  end
106
209
 
107
210
  def rewrite_response(triplet)
108
- status, headers, body = triplet
211
+ _, headers, _ = triplet
109
212
 
110
213
  # if you rewrite env, it appears that content-length isn't calculated correctly
111
214
  # resulting in only partial responses being sent to users
@@ -114,19 +217,20 @@ class TrustingProxy < Rack::Proxy
114
217
 
115
218
  triplet
116
219
  end
117
-
118
220
  end
119
- ```
120
-
121
- The same can be achieved for *all* requests going through the `Rack::Proxy` instance by using
122
221
 
123
- ```ruby
124
- Rack::Proxy.new(ssl_verify_none: true)
222
+ # Mount it with an explicit backend (dynamic Host-derived backends are refused
223
+ # by default since 1.0). Pass ssl_verify_none: true to skip TLS verification.
224
+ Rails.application.config.middleware.use TrustingProxy,
225
+ backend: "https://self-signed.badssl.com",
226
+ ssl_verify_none: true
125
227
  ```
126
228
 
127
- ### Rails middleware example
229
+ For a backend signed by a private CA, prefer `ca_file: "/path/to/ca.pem"` over disabling verification.
230
+
231
+ ### Mount an external service under a path (Rails)
128
232
 
129
- Test with `require 'rack_proxy_examples/example_service_proxy'`
233
+ From [`examples/example_service_proxy.rb`](examples/example_service_proxy.rb):
130
234
 
131
235
  ```ruby
132
236
  ###
@@ -136,32 +240,32 @@ Test with `require 'rack_proxy_examples/example_service_proxy'`
136
240
  # 1. rails new test_app
137
241
  # 2. cd test_app
138
242
  # 3. install Rack-Proxy in `Gemfile`
139
- # a. `gem 'rack-proxy', '~> 0.7.7'`
243
+ # a. `gem 'rack-proxy', '~> 1.0'`
140
244
  # 4. install gem: `bundle install`
141
- # 5. create `config/initializers/proxy.rb` adding this line `require 'rack_proxy_examples/example_service_proxy'`
245
+ # 5. copy the class into your app and mount it from `config/initializers/proxy.rb`
142
246
  # 6. run: `SERVICE_URL=http://guides.rubyonrails.org rails server`
143
247
  # 7. open in browser: `http://localhost:3000/example_service`
144
248
  #
145
249
  ###
146
- ENV['SERVICE_URL'] ||= 'http://guides.rubyonrails.org'
250
+ ENV["SERVICE_URL"] ||= "http://guides.rubyonrails.org"
147
251
 
148
252
  class ExampleServiceProxy < Rack::Proxy
149
253
  def perform_request(env)
150
254
  request = Rack::Request.new(env)
151
255
 
152
256
  # use rack proxy for anything hitting our host app at /example_service
153
- if request.path =~ %r{^/example_service}
154
- backend = URI(ENV['SERVICE_URL'])
155
- # most backends required host set properly, but rack-proxy doesn't set this for you automatically
156
- # even when a backend host is passed in via the options
157
- env["HTTP_HOST"] = backend.host
158
-
159
- # This is the only path that needs to be set currently on Rails 5 & greater
160
- env['PATH_INFO'] = ENV['SERVICE_PATH'] || '/configuring.html'
161
-
162
- # don't send your sites cookies to target service, unless it is a trusted internal service that can parse all your cookies
163
- env['HTTP_COOKIE'] = ''
164
- super(env)
257
+ if %r{^/example_service}.match?(request.path)
258
+ backend = URI(ENV["SERVICE_URL"])
259
+ # most backends required host set properly, but rack-proxy doesn't set this for you automatically
260
+ # even when a backend host is passed in via the options
261
+ env["HTTP_HOST"] = backend.host
262
+
263
+ # This is the only path that needs to be set currently on Rails 5 & greater
264
+ env["PATH_INFO"] = ENV["SERVICE_PATH"] || "/configuring.html"
265
+
266
+ # don't send your sites cookies to target service, unless it is a trusted internal service that can parse all your cookies
267
+ env["HTTP_COOKIE"] = ""
268
+ super
165
269
  else
166
270
  @app.call(env)
167
271
  end
@@ -169,39 +273,36 @@ class ExampleServiceProxy < Rack::Proxy
169
273
  end
170
274
  ```
171
275
 
172
- ### Using as middleware to forward only some extensions to another Application
173
-
174
- Test with `require 'rack_proxy_examples/rack_php_proxy'`
276
+ ### Proxy only matching requests (e.g. `.php`) as middleware
175
277
 
176
- Example: Proxying only requests that end with ".php" could be done like this:
278
+ From [`examples/rack_php_proxy.rb`](examples/rack_php_proxy.rb):
177
279
 
178
280
  ```ruby
179
281
  ###
180
282
  # Open http://localhost:3000/test.php to trigger proxy
181
283
  ###
182
284
  class RackPhpProxy < Rack::Proxy
183
-
184
285
  def perform_request(env)
185
286
  request = Rack::Request.new(env)
186
- if request.path =~ %r{\.php}
287
+ if %r{\.php}.match?(request.path)
187
288
  env["HTTP_HOST"] = ENV["HTTP_HOST"] ? URI(ENV["HTTP_HOST"]).host : "localhost"
188
- ENV["PHP_PATH"] ||= '/manual/en/tutorial.firstpage.php'
289
+ ENV["PHP_PATH"] ||= "/manual/en/tutorial.firstpage.php"
189
290
 
190
291
  # Rails 3 & 4
191
292
  env["REQUEST_PATH"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
192
293
  # Rails 5 and above
193
- env['PATH_INFO'] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
294
+ env["PATH_INFO"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
194
295
 
195
- env['content-length'] = nil
296
+ env["content-length"] = nil
196
297
 
197
- super(env)
298
+ super
198
299
  else
199
300
  @app.call(env)
200
301
  end
201
302
  end
202
303
 
203
304
  def rewrite_response(triplet)
204
- status, headers, body = triplet
305
+ _, headers, _ = triplet
205
306
 
206
307
  # if you proxy depending on the backend, it appears that content-length isn't calculated correctly
207
308
  # resulting in only partial responses being sent to users
@@ -213,113 +314,65 @@ class RackPhpProxy < Rack::Proxy
213
314
  end
214
315
  ```
215
316
 
216
- To use the middleware, please consider the following:
217
-
218
- 1) For Rails we could add a configuration in `config/application.rb`
317
+ Mount it in Rails (`config/application.rb`):
219
318
 
220
319
  ```ruby
221
- config.middleware.use RackPhpProxy, {ssl_verify_none: true}
320
+ config.middleware.use RackPhpProxy, backend: "http://php.net"
222
321
  ```
223
322
 
224
- 2) For Sinatra or any Rack-based application:
323
+ or in Sinatra / any Rack app:
225
324
 
226
325
  ```ruby
227
326
  class MyAwesomeSinatra < Sinatra::Base
228
- use RackPhpProxy, {ssl_verify_none: true}
327
+ use RackPhpProxy, backend: "http://php.net"
229
328
  end
230
329
  ```
231
330
 
232
- This will allow to run the other requests through the application and only proxy the requests that match the condition from the middleware.
331
+ Requests matching the condition are proxied; everything else runs through your application as usual. See the tests for more examples.
233
332
 
234
- See tests for more examples.
333
+ ### Local TLS-terminating proxy
235
334
 
236
- ### SSL proxy for SpringBoot applications debugging
335
+ Useful when an external integration (OAuth callbacks, webhooks) insists on talking HTTPS to your dev machine: terminate TLS locally and forward the decrypted traffic to your app running on plain HTTP.
237
336
 
238
- 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.
239
-
240
- Create `config.ru` file:
241
337
  ```ruby
242
- #
243
338
  # config.ru
244
- #
245
- require 'rack'
246
- require 'rack-proxy'
339
+ require "rack-proxy"
247
340
 
248
- class ForwardHost < Rack::Proxy
341
+ class ForwardProto < Rack::Proxy
249
342
  def rewrite_env(env)
250
- env['HTTP_X_FORWARDED_HOST'] = env['SERVER_NAME']
251
- env['HTTP_X_FORWARDED_PROTO'] = env['rack.url_scheme']
343
+ env["HTTP_X_FORWARDED_HOST"] = env["SERVER_NAME"]
344
+ env["HTTP_X_FORWARDED_PROTO"] = env["rack.url_scheme"]
252
345
  env
253
346
  end
254
347
  end
255
348
 
256
- run ForwardHost.new(backend: 'http://localhost:8080')
257
- ```
258
-
259
- Create `Gemfile` file:
260
- ```ruby
261
- source "https://rubygems.org"
262
-
263
- gem 'thin'
264
- gem 'rake'
265
- gem 'rack-proxy'
349
+ run ForwardProto.new(backend: "http://localhost:8080")
266
350
  ```
267
351
 
268
- Create `config.yml` file with configuration of web server `thin`:
269
- ```yml
270
- ---
271
- ssl: true
272
- ssl-key-file: keys/domain.key
273
- ssl-cert-file: keys/domain.crt
274
- ssl-disable-verify: false
275
- ```
276
-
277
- Create 'keys' directory and generate SSL key and certificates files `domain.key` and `domain.crt`
278
-
279
- Run `bundle exec thin start` for running it with `thin`'s default port.
280
-
281
- Or use `sudo -E thin start -C config.yml -p 443` for running with default for `https://` port.
352
+ Generate a key/certificate pair for your dev hostname and serve the proxy over TLS, e.g. with puma:
282
353
 
283
- Don't forget to enable processing of `X-Forwarded-...` headers on your application side. Just add following strings to your `resources/application.yml` file.
284
- ```yml
285
- ---
286
- server:
287
- tomcat:
288
- remote-ip-header: x-forwarded-for
289
- protocol-header: x-forwarded-proto
290
- use-forward-headers: true
354
+ ```sh
355
+ puma -b 'ssl://0.0.0.0:9292?key=keys/domain.key&cert=keys/domain.crt' config.ru
291
356
  ```
292
357
 
293
- Add some domain name like `debug.your_app.com` into your local `/etc/hosts` file like
294
- ```
295
- 127.0.0.1 debug.your_app.com
296
- ```
358
+ Point the dev hostname at yourself (`127.0.0.1 debug.your_app.com` in `/etc/hosts`), and make sure your app honors `X-Forwarded-Host` / `X-Forwarded-Proto` from this trusted hop (e.g. `server.forward-headers-strategy: framework` in Spring Boot, or Rails' default `config.action_dispatch` handling).
297
359
 
298
- 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.
360
+ ### Client TLS certificates (mutual TLS)
299
361
 
300
- ### Using SSL/TLS certificates with HTTP connection
301
- 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.
362
+ When a third-party API authenticates clients with TLS certificates, terminate the client's request and re-sign the outgoing connection:
302
363
 
303
- Just specify Rack::Proxy SSL options and your request will use TLS HTTP connection:
304
364
  ```ruby
305
365
  # config.ru
306
- . . .
307
-
308
- cert_raw = File.read('./certs/rootCA.crt')
309
- key_raw = File.read('./certs/key.pem')
310
-
311
- cert = OpenSSL::X509::Certificate.new(cert_raw)
312
- key = OpenSSL::PKey.read(key_raw)
366
+ cert = OpenSSL::X509::Certificate.new(File.read("./certs/client.crt"))
367
+ key = OpenSSL::PKey.read(File.read("./certs/key.pem"))
313
368
 
314
- use TLSProxy, cert: cert, key: key, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER, ssl_version: 'TLSv1_2'
369
+ use TLSProxy, backend: "https://client-tls-auth-api.com",
370
+ cert: cert, key: key, min_version: :TLS1_2
315
371
  ```
316
372
 
317
- And rewrite host for example:
318
373
  ```ruby
319
374
  # tls_proxy.rb
320
375
  class TLSProxy < Rack::Proxy
321
- attr_accessor :original_request, :query_params
322
-
323
376
  def rewrite_env(env)
324
377
  env["HTTP_HOST"] = "client-tls-auth-api.com:443"
325
378
  env
@@ -327,14 +380,68 @@ class TLSProxy < Rack::Proxy
327
380
  end
328
381
  ```
329
382
 
330
- WARNING
331
- ----
383
+ ## Upgrading
384
+
385
+ ### 0.8.x → 1.0.0
386
+
387
+ 1.0.0 is a breaking release; the full list is in [CHANGELOG.md](CHANGELOG.md). The changes most likely to need action:
388
+
389
+ **Host-derived backends now require an explicit opt-in.** If you rely on the destination being derived from the request's `Host` header (no `:backend` option — this includes every subclass that routes by rewriting `env["HTTP_HOST"]`), such requests now return `502`. Restore the behavior explicitly, ideally with an allowlist:
390
+
391
+ ```ruby
392
+ class MyProxy < Rack::Proxy
393
+ def backend_allowed?(backend)
394
+ %w[api.internal.example.com].include?(backend.host)
395
+ end
396
+ end
397
+
398
+ MyProxy.new(allow_dynamic_backend: true)
399
+ ```
400
+
401
+ Deployments with a fixed `:backend` (or that set `env["rack.backend"]` in `rewrite_env`) need no change.
402
+
403
+ **Backend failures return `502` instead of raising.** If you rescued `OpenSSL::SSL::SSLError`, `Errno::ECONNREFUSED`, timeouts, etc. around the proxy, inspect the response status instead. Malformed request URIs map to `400` and unknown HTTP methods to `501`.
404
+
405
+ **`require "rack_proxy_examples/..."` is gone.** The examples are copy-paste snippets in [`examples/`](examples/) now — copy the class into your app and mount it yourself.
406
+
407
+ **`net_http_hacked` is gone.** The library streams via the public `Net::HTTP` API; if external code called `begin_request_hacked`/`end_request_hacked`, vendor the old file from a 0.8.x release and plan a migration.
408
+
409
+ **Other behavior changes to be aware of:** hop-by-hop request headers (including `Proxy-Authorization`) are no longer forwarded; gzip bodies are forwarded still-compressed in `streaming: false` mode (inflate in `rewrite_response` if you inspect body text); body-less POST/PUT sends `Content-Length: 0`; Ruby >= 3.0 and Rack 2.x–3.x are required.
410
+
411
+ ### 0.7.x → 0.8.0
412
+
413
+ **TLS certificate verification is now on by default.** Prior versions silently used `OpenSSL::SSL::VERIFY_NONE` whenever the backend was HTTPS, which disabled certificate checks. 0.8.0 defaults to `VERIFY_PEER` to match Ruby's `Net::HTTP`.
414
+
415
+ If you proxy to a backend with a self-signed or otherwise untrusted certificate, you'll now get an `OpenSSL::SSL::SSLError` unless you opt out explicitly:
416
+
417
+ ```ruby
418
+ Rack::Proxy.new(ssl_verify_none: true) # or
419
+ Rack::Proxy.new(verify_mode: OpenSSL::SSL::VERIFY_NONE)
420
+ ```
421
+
422
+ For internal services with a private CA, prefer setting `ca_file`/`cert_store` over disabling verification altogether.
423
+
424
+ ## Header keys and underscores
425
+
426
+ Per the standard Rack/CGI convention, header names received by your proxy are exposed in the env with underscores (`HTTP_X_CUSTOM_HEADER`), and rack-proxy rewrites them with dashes (`X-Custom-Header`) when forwarding. This conversion is lossy: by the time a request reaches rack-proxy, the upstream web server (nginx, Apache, Caddy, Puma) has already collapsed both `X-Custom-Header` and `X_Custom_Header` into the same env key, and rack-proxy cannot recover the original spelling (see [#96](https://github.com/ncr/rack-proxy/issues/96)).
427
+
428
+ If you need underscore-style headers preserved end-to-end, configure your fronting web server (e.g. `underscores_in_headers on;` in nginx, or `HTTPProtocolOptions` in Apache) — rack-proxy is not the right layer to fix this.
429
+
430
+ ## Compatibility notes
431
+
432
+ The streaming response path (the default) streams straight off the backend socket via `Net::HTTP`. Historically it relied on private `net/http` internals and did not work at all under `webmock`, `vcr`, or `fakeweb`; it now uses only the public `Net::HTTP#request` API, but those libraries still replace the real network layer, so behavior under them is not guaranteed. In tests that stub HTTP, prefer `streaming: false`.
433
+
434
+ ## Development
435
+
436
+ ```sh
437
+ bundle install
438
+ bundle exec rake test # full suite, fully offline, ~2-3s
439
+ LIVE=1 bundle exec rake test # additionally runs real-internet smoke tests
440
+ bundle exec standardrb # style check (CI-blocking)
441
+ ```
332
442
 
333
- Doesn't work with `fakeweb`/`webmock`. Both libraries monkey-patch net/http code.
443
+ Bug reports and pull requests are welcome see [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow and [CLAUDE.md](CLAUDE.md) for the invariants every change must preserve. Release history lives in [CHANGELOG.md](CHANGELOG.md).
334
444
 
335
- Todos
336
- ----
445
+ ## License
337
446
 
338
- * 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.
339
- * Improve and validate requirements for Host and Path rewrite rules
340
- * Ability to inject logger and set log level
447
+ Released under the [MIT License](LICENSE).
data/SECURITY.md ADDED
@@ -0,0 +1,52 @@
1
+ # Security Policy
2
+
3
+ `rack-proxy` is a request/response-rewriting HTTP proxy. Because it forwards
4
+ attacker-influenced requests to a backend and relays the backend's response, how
5
+ you configure and subclass it has direct security consequences. Please read the
6
+ threat model below alongside the "Security considerations" section of the README.
7
+
8
+ ## Supported versions
9
+
10
+ Security fixes are released for the latest major series. The last `0.x` series
11
+ receives fixes for critical issues only, for a transition period — please
12
+ upgrade to `1.x`.
13
+
14
+ | Version | Supported |
15
+ | ------- | --------- |
16
+ | 1.0.x | ✅ |
17
+ | 0.8.x | critical fixes only |
18
+ | < 0.8 | ❌ |
19
+
20
+ ## Reporting a vulnerability
21
+
22
+ **Please do not open a public issue for security problems.**
23
+
24
+ Report privately through GitHub's **Report a vulnerability** button under the
25
+ repository's *Security* tab (Private Vulnerability Reporting). If that is
26
+ unavailable to you, email the maintainer at **jacek.becela@gmail.com** with
27
+ `[rack-proxy security]` in the subject.
28
+
29
+ Please include:
30
+
31
+ - the rack-proxy, Rack, and Ruby versions,
32
+ - whether you run in streaming (`streaming: true`, the default) or non-streaming mode,
33
+ - a minimal `config.ru` / subclass that reproduces the issue,
34
+ - the impact you observed.
35
+
36
+ We aim to acknowledge a report within **5 business days** and to agree on a
37
+ disclosure timeline from there. We are grateful for responsible disclosure and
38
+ will credit reporters who want it.
39
+
40
+ ## Scope
41
+
42
+ In scope: defects in the library itself — for example, credentials or hop-by-hop
43
+ headers being forwarded when they should not be, request/response smuggling,
44
+ verification defaults that are weaker than documented, or a crash/`500` where a
45
+ `4xx`/`5xx` mapping is expected.
46
+
47
+ Out of scope: insecure **configuration or subclassing** of the library. Since
48
+ 1.0, deriving the backend from the client-controlled `Host` header requires an
49
+ explicit `allow_dynamic_backend: true`; opting in without a `backend_allowed?`
50
+ allowlist is an SSRF/open-proxy risk that is the deployer's responsibility —
51
+ see the README "Security considerations". If the documentation is what led you
52
+ astray, that is in scope: tell us and we will fix the docs.