rack-proxy 0.7.7 → 2.0.0

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", "~> 2.0"
8
41
  ```
9
- gem 'rack-proxy', '~> 0.7.7'
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")
10
51
  ```
11
52
 
12
- Or install:
53
+ As middleware, subclass it and decide per request: call `super` to proxy, or hand the request to the rest of your app:
13
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"
14
72
  ```
15
- gem install rack-proxy
73
+
74
+ ## How it works
75
+
76
+ Every request runs through a three-step pipeline:
77
+
78
+ ```
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 body. Oversized declared lengths are refused before reading the body, and each chunk is checked before buffering or forwarding it. Non-streaming responses return `502` on overflow; streaming responses abort if overflow is discovered after sending the headers. HEAD/304 representation lengths do not count as body bytes.
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).
150
+
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.
62
152
 
63
- ### Forward request to Host and Insert Header
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.
64
154
 
65
- Test with `require 'rack_proxy_examples/forward_host'`
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
+ ```
93
198
 
94
- Test with `require 'rack_proxy_examples/trusting_proxy'`
199
+ ### Proxy to a backend with a self-signed certificate
200
+
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
221
 
121
- The same can be achieved for *all* requests going through the `Rack::Proxy` instance by using
122
-
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.
128
230
 
129
- Test with `require 'rack_proxy_examples/example_service_proxy'`
231
+ ### Mount an external service under a path (Rails)
232
+
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', '~> 2.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,128 +314,145 @@ 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.
233
-
234
- See tests for more examples.
331
+ Requests matching the condition are proxied; everything else runs through your application as usual. See the tests for more examples.
235
332
 
236
- ### SSL proxy for SpringBoot applications debugging
333
+ ### Local TLS-terminating proxy
237
334
 
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.
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.
239
336
 
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')
349
+ run ForwardProto.new(backend: "http://localhost:8080")
257
350
  ```
258
351
 
259
- Create `Gemfile` file:
260
- ```ruby
261
- source "https://rubygems.org"
352
+ Generate a key/certificate pair for your dev hostname and serve the proxy over TLS, e.g. with puma:
262
353
 
263
- gem 'thin'
264
- gem 'rake'
265
- gem 'rack-proxy'
354
+ ```sh
355
+ puma -b 'ssl://0.0.0.0:9292?key=keys/domain.key&cert=keys/domain.crt' config.ru
266
356
  ```
267
357
 
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
- ```
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).
276
359
 
277
- Create 'keys' directory and generate SSL key and certificates files `domain.key` and `domain.crt`
360
+ ### Client TLS certificates (mutual TLS)
278
361
 
279
- Run `bundle exec thin start` for running it with `thin`'s default port.
362
+ When a third-party API authenticates clients with TLS certificates, terminate the client's request and re-sign the outgoing connection:
280
363
 
281
- Or use `sudo -E thin start -C config.yml -p 443` for running with default for `https://` port.
364
+ ```ruby
365
+ # config.ru
366
+ cert = OpenSSL::X509::Certificate.new(File.read("./certs/client.crt"))
367
+ key = OpenSSL::PKey.read(File.read("./certs/key.pem"))
282
368
 
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
369
+ use TLSProxy, backend: "https://client-tls-auth-api.com",
370
+ cert: cert, key: key, min_version: :TLS1_2
291
371
  ```
292
372
 
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
373
+ ```ruby
374
+ # tls_proxy.rb
375
+ class TLSProxy < Rack::Proxy
376
+ def rewrite_env(env)
377
+ env["HTTP_HOST"] = "client-tls-auth-api.com:443"
378
+ env
379
+ end
380
+ end
296
381
  ```
297
382
 
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.
383
+ ## Upgrading
299
384
 
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.
385
+ ### 1.x 2.0.0
302
386
 
303
- Just specify Rack::Proxy SSL options and your request will use TLS HTTP connection:
304
- ```ruby
305
- # config.ru
306
- . . .
387
+ 2.0.0 hardens HTTP framing and response limits. Ruby and Rack requirements are unchanged.
307
388
 
308
- cert_raw = File.read('./certs/rootCA.crt')
309
- key_raw = File.read('./certs/key.pem')
389
+ - **Automatic transport retries are disabled in both modes.** Non-streaming requests previously inherited Net::HTTP's retry behavior. If your application needs retries, use an explicit policy that considers whether the operation is safe to replay and provides a fresh request body for each attempt.
390
+ - **Response hooks follow Rack's types.** Statuses are integers in both modes. On Rack 3, repeated headers such as `Set-Cookie` are arrays of strings; update hooks that split these values on newlines. Rack 2 still uses newline-separated strings.
391
+ - **Framing errors fail explicitly.** Malformed request lengths and incomplete uploads return `400`. Ambiguous backend framing returns `502`. A truncated backend body returns `502` before headers are sent, or raises during streaming so the server aborts the transfer. Uploads without `CONTENT_LENGTH` are forwarded with chunked encoding; backends must support HTTP/1.1 chunked requests.
392
+ - **Response limits apply before buffering.** `max_response_length` now bounds non-streaming accumulation. HEAD and 304 representation lengths do not count toward the body cap.
310
393
 
311
- cert = OpenSSL::X509::Certificate.new(cert_raw)
312
- key = OpenSSL::PKey.read(key_raw)
394
+ Update your Gemfile to `gem "rack-proxy", "~> 2.0"` and run `bundle update rack-proxy`. Existing explicit backend and TLS options continue to work.
313
395
 
314
- use TLSProxy, cert: cert, key: key, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER, ssl_version: 'TLSv1_2'
315
- ```
396
+ ### 0.8.x 1.0.0
316
397
 
317
- And rewrite host for example:
318
- ```ruby
319
- # tls_proxy.rb
320
- class TLSProxy < Rack::Proxy
321
- attr_accessor :original_request, :query_params
398
+ 1.0.0 is a breaking release; the full list is in [CHANGELOG.md](CHANGELOG.md). The changes most likely to need action:
322
399
 
323
- def rewrite_env(env)
324
- env["HTTP_HOST"] = "client-tls-auth-api.com:443"
325
- env
400
+ **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:
401
+
402
+ ```ruby
403
+ class MyProxy < Rack::Proxy
404
+ def backend_allowed?(backend)
405
+ %w[api.internal.example.com].include?(backend.host)
326
406
  end
327
407
  end
408
+
409
+ MyProxy.new(allow_dynamic_backend: true)
410
+ ```
411
+
412
+ Deployments with a fixed `:backend` (or that set `env["rack.backend"]` in `rewrite_env`) need no change.
413
+
414
+ **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`.
415
+
416
+ **`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.
417
+
418
+ **`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.
419
+
420
+ **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.
421
+
422
+ ### 0.7.x → 0.8.0
423
+
424
+ **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`.
425
+
426
+ 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:
427
+
428
+ ```ruby
429
+ Rack::Proxy.new(ssl_verify_none: true) # or
430
+ Rack::Proxy.new(verify_mode: OpenSSL::SSL::VERIFY_NONE)
328
431
  ```
329
432
 
330
- WARNING
331
- ----
433
+ For internal services with a private CA, prefer setting `ca_file`/`cert_store` over disabling verification altogether.
434
+
435
+ ## Header keys and underscores
436
+
437
+ 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)).
438
+
439
+ 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.
440
+
441
+ ## Compatibility notes
442
+
443
+ 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`.
444
+
445
+ ## Development
446
+
447
+ ```sh
448
+ bundle install
449
+ bundle exec rake test # full suite, fully offline, ~2-3s
450
+ LIVE=1 bundle exec rake test # additionally runs real-internet smoke tests
451
+ bundle exec standardrb # style check (CI-blocking)
452
+ ```
332
453
 
333
- Doesn't work with `fakeweb`/`webmock`. Both libraries monkey-patch net/http code.
454
+ 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
455
 
335
- Todos
336
- ----
456
+ ## License
337
457
 
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
458
+ Released under the [MIT License](LICENSE).