rack-proxy 0.8.3 → 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,81 +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.8.0'
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 skip TLS certificate verification (defaults to verifying — see [Upgrading](#upgrading) for the 0.8 change)
42
- * `:verify_mode` - explicit `OpenSSL::SSL::VERIFY_*` constant; wins over `ssl_verify_none`
43
- * `:ssl_version` - tell `Net::HTTP` to set a specific `ssl_version`
44
- * `: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.
45
- * `:read_timeout` - set proxy timeout it defaults to 60 seconds
46
- * `:logger` - any object responding to `#<<` (e.g. `$stdout`, a `StringIO`, or a Ruby `Logger`). Wired through to `Net::HTTP#set_debug_output` so the full HTTP wire-level conversation is written to the sink. Useful for debugging.
98
+ ### Routing and mode
47
99
 
48
- 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)).
49
103
 
50
- ```ruby
51
- Rails.application.config.middleware.use ExampleServiceProxy, backend: 'http://guides.rubyonrails.org', streaming: false
52
- ```
104
+ ### TLS
53
105
 
54
- Examples
55
- ----
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`.
56
113
 
57
- See and run the examples below from `lib/rack_proxy_examples/`. To mount any example into an existing Rails app:
114
+ ### Timeouts and limits
58
115
 
59
- 1. create `config/initializers/proxy.rb`
60
- 2. modify the file to require the example file
61
- ```ruby
62
- require 'rack_proxy_examples/forward_host'
63
- ```
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).
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.
152
+
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).
64
162
 
65
- ### Forward request to Host and Insert Header
163
+ ## Recipes
66
164
 
67
- Test with `require 'rack_proxy_examples/forward_host'`
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):
68
170
 
69
171
  ```ruby
70
172
  class ForwardHost < Rack::Proxy
71
-
72
173
  def rewrite_env(env)
73
174
  env["HTTP_HOST"] = "example.com"
74
175
  env
75
176
  end
76
177
 
77
178
  def rewrite_response(triplet)
78
- status, headers, body = triplet
179
+ _, headers, _ = triplet
79
180
 
80
181
  # example of inserting an additional header
81
182
  headers["X-Foo"] = "Bar"
@@ -87,24 +188,27 @@ class ForwardHost < Rack::Proxy
87
188
 
88
189
  triplet
89
190
  end
90
-
91
191
  end
92
192
  ```
93
193
 
94
- ### 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
95
200
 
96
- Test with `require 'rack_proxy_examples/trusting_proxy'`
201
+ From [`examples/trusting_proxy.rb`](examples/trusting_proxy.rb):
97
202
 
98
203
  ```ruby
99
204
  class TrustingProxy < Rack::Proxy
100
-
101
205
  def rewrite_env(env)
102
206
  env["HTTP_HOST"] = "self-signed.badssl.com"
103
207
  env
104
208
  end
105
209
 
106
210
  def rewrite_response(triplet)
107
- status, headers, body = triplet
211
+ _, headers, _ = triplet
108
212
 
109
213
  # if you rewrite env, it appears that content-length isn't calculated correctly
110
214
  # resulting in only partial responses being sent to users
@@ -113,16 +217,20 @@ class TrustingProxy < Rack::Proxy
113
217
 
114
218
  triplet
115
219
  end
116
-
117
220
  end
118
221
 
119
- # Pass ssl_verify_none: true to skip TLS certificate verification.
120
- 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
121
227
  ```
122
228
 
123
- ### Rails middleware example
229
+ For a backend signed by a private CA, prefer `ca_file: "/path/to/ca.pem"` over disabling verification.
124
230
 
125
- 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):
126
234
 
127
235
  ```ruby
128
236
  ###
@@ -132,32 +240,32 @@ Test with `require 'rack_proxy_examples/example_service_proxy'`
132
240
  # 1. rails new test_app
133
241
  # 2. cd test_app
134
242
  # 3. install Rack-Proxy in `Gemfile`
135
- # a. `gem 'rack-proxy', '~> 0.7.7'`
243
+ # a. `gem 'rack-proxy', '~> 1.0'`
136
244
  # 4. install gem: `bundle install`
137
- # 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`
138
246
  # 6. run: `SERVICE_URL=http://guides.rubyonrails.org rails server`
139
247
  # 7. open in browser: `http://localhost:3000/example_service`
140
248
  #
141
249
  ###
142
- ENV['SERVICE_URL'] ||= 'http://guides.rubyonrails.org'
250
+ ENV["SERVICE_URL"] ||= "http://guides.rubyonrails.org"
143
251
 
144
252
  class ExampleServiceProxy < Rack::Proxy
145
253
  def perform_request(env)
146
254
  request = Rack::Request.new(env)
147
255
 
148
256
  # use rack proxy for anything hitting our host app at /example_service
149
- if request.path =~ %r{^/example_service}
150
- backend = URI(ENV['SERVICE_URL'])
151
- # most backends required host set properly, but rack-proxy doesn't set this for you automatically
152
- # even when a backend host is passed in via the options
153
- env["HTTP_HOST"] = backend.host
154
-
155
- # This is the only path that needs to be set currently on Rails 5 & greater
156
- env['PATH_INFO'] = ENV['SERVICE_PATH'] || '/configuring.html'
157
-
158
- # don't send your sites cookies to target service, unless it is a trusted internal service that can parse all your cookies
159
- env['HTTP_COOKIE'] = ''
160
- 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
161
269
  else
162
270
  @app.call(env)
163
271
  end
@@ -165,39 +273,36 @@ class ExampleServiceProxy < Rack::Proxy
165
273
  end
166
274
  ```
167
275
 
168
- ### Using as middleware to forward only some extensions to another Application
169
-
170
- Test with `require 'rack_proxy_examples/rack_php_proxy'`
276
+ ### Proxy only matching requests (e.g. `.php`) as middleware
171
277
 
172
- 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):
173
279
 
174
280
  ```ruby
175
281
  ###
176
282
  # Open http://localhost:3000/test.php to trigger proxy
177
283
  ###
178
284
  class RackPhpProxy < Rack::Proxy
179
-
180
285
  def perform_request(env)
181
286
  request = Rack::Request.new(env)
182
- if request.path =~ %r{\.php}
287
+ if %r{\.php}.match?(request.path)
183
288
  env["HTTP_HOST"] = ENV["HTTP_HOST"] ? URI(ENV["HTTP_HOST"]).host : "localhost"
184
- ENV["PHP_PATH"] ||= '/manual/en/tutorial.firstpage.php'
289
+ ENV["PHP_PATH"] ||= "/manual/en/tutorial.firstpage.php"
185
290
 
186
291
  # Rails 3 & 4
187
292
  env["REQUEST_PATH"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
188
293
  # Rails 5 and above
189
- env['PATH_INFO'] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
294
+ env["PATH_INFO"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
190
295
 
191
- env['content-length'] = nil
296
+ env["content-length"] = nil
192
297
 
193
- super(env)
298
+ super
194
299
  else
195
300
  @app.call(env)
196
301
  end
197
302
  end
198
303
 
199
304
  def rewrite_response(triplet)
200
- status, headers, body = triplet
305
+ _, headers, _ = triplet
201
306
 
202
307
  # if you proxy depending on the backend, it appears that content-length isn't calculated correctly
203
308
  # resulting in only partial responses being sent to users
@@ -209,113 +314,65 @@ class RackPhpProxy < Rack::Proxy
209
314
  end
210
315
  ```
211
316
 
212
- To use the middleware, please consider the following:
213
-
214
- 1) For Rails we could add a configuration in `config/application.rb`
317
+ Mount it in Rails (`config/application.rb`):
215
318
 
216
319
  ```ruby
217
- config.middleware.use RackPhpProxy, {ssl_verify_none: true}
320
+ config.middleware.use RackPhpProxy, backend: "http://php.net"
218
321
  ```
219
322
 
220
- 2) For Sinatra or any Rack-based application:
323
+ or in Sinatra / any Rack app:
221
324
 
222
325
  ```ruby
223
326
  class MyAwesomeSinatra < Sinatra::Base
224
- use RackPhpProxy, {ssl_verify_none: true}
327
+ use RackPhpProxy, backend: "http://php.net"
225
328
  end
226
329
  ```
227
330
 
228
- This will allow to run the other requests through the application and only proxy the requests that match the condition from the middleware.
229
-
230
- 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.
231
332
 
232
- ### SSL proxy for SpringBoot applications debugging
333
+ ### Local TLS-terminating proxy
233
334
 
234
- 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.
235
336
 
236
- Create `config.ru` file:
237
337
  ```ruby
238
- #
239
338
  # config.ru
240
- #
241
- require 'rack'
242
- require 'rack-proxy'
339
+ require "rack-proxy"
243
340
 
244
- class ForwardHost < Rack::Proxy
341
+ class ForwardProto < Rack::Proxy
245
342
  def rewrite_env(env)
246
- env['HTTP_X_FORWARDED_HOST'] = env['SERVER_NAME']
247
- 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"]
248
345
  env
249
346
  end
250
347
  end
251
348
 
252
- run ForwardHost.new(backend: 'http://localhost:8080')
253
- ```
254
-
255
- Create `Gemfile` file:
256
- ```ruby
257
- source "https://rubygems.org"
258
-
259
- gem 'thin'
260
- gem 'rake'
261
- gem 'rack-proxy'
349
+ run ForwardProto.new(backend: "http://localhost:8080")
262
350
  ```
263
351
 
264
- Create `config.yml` file with configuration of web server `thin`:
265
- ```yml
266
- ---
267
- ssl: true
268
- ssl-key-file: keys/domain.key
269
- ssl-cert-file: keys/domain.crt
270
- ssl-disable-verify: false
271
- ```
272
-
273
- Create 'keys' directory and generate SSL key and certificates files `domain.key` and `domain.crt`
274
-
275
- Run `bundle exec thin start` for running it with `thin`'s default port.
352
+ Generate a key/certificate pair for your dev hostname and serve the proxy over TLS, e.g. with puma:
276
353
 
277
- Or use `sudo -E thin start -C config.yml -p 443` for running with default for `https://` port.
278
-
279
- Don't forget to enable processing of `X-Forwarded-...` headers on your application side. Just add following strings to your `resources/application.yml` file.
280
- ```yml
281
- ---
282
- server:
283
- tomcat:
284
- remote-ip-header: x-forwarded-for
285
- protocol-header: x-forwarded-proto
286
- 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
287
356
  ```
288
357
 
289
- Add some domain name like `debug.your_app.com` into your local `/etc/hosts` file like
290
- ```
291
- 127.0.0.1 debug.your_app.com
292
- ```
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).
293
359
 
294
- 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)
295
361
 
296
- ### Using SSL/TLS certificates with HTTP connection
297
- 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:
298
363
 
299
- Just specify Rack::Proxy SSL options and your request will use TLS HTTP connection:
300
364
  ```ruby
301
365
  # config.ru
302
- . . .
366
+ cert = OpenSSL::X509::Certificate.new(File.read("./certs/client.crt"))
367
+ key = OpenSSL::PKey.read(File.read("./certs/key.pem"))
303
368
 
304
- cert_raw = File.read('./certs/rootCA.crt')
305
- key_raw = File.read('./certs/key.pem')
306
-
307
- cert = OpenSSL::X509::Certificate.new(cert_raw)
308
- key = OpenSSL::PKey.read(key_raw)
309
-
310
- 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
311
371
  ```
312
372
 
313
- And rewrite host for example:
314
373
  ```ruby
315
374
  # tls_proxy.rb
316
375
  class TLSProxy < Rack::Proxy
317
- attr_accessor :original_request, :query_params
318
-
319
376
  def rewrite_env(env)
320
377
  env["HTTP_HOST"] = "client-tls-auth-api.com:443"
321
378
  env
@@ -323,8 +380,33 @@ class TLSProxy < Rack::Proxy
323
380
  end
324
381
  ```
325
382
 
326
- Upgrading
327
- ----
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.
328
410
 
329
411
  ### 0.7.x → 0.8.0
330
412
 
@@ -337,23 +419,29 @@ Rack::Proxy.new(ssl_verify_none: true) # or
337
419
  Rack::Proxy.new(verify_mode: OpenSSL::SSL::VERIFY_NONE)
338
420
  ```
339
421
 
340
- For internal services with a private CA, prefer setting `cert`/`verify_mode` over disabling verification altogether.
422
+ For internal services with a private CA, prefer setting `ca_file`/`cert_store` over disabling verification altogether.
341
423
 
342
- A note on header keys (#96)
343
- ----
424
+ ## Header keys and underscores
344
425
 
345
- 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.
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)).
346
427
 
347
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.
348
429
 
349
- WARNING
350
- ----
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
+ ```
351
442
 
352
- 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).
353
444
 
354
- Todos
355
- ----
445
+ ## License
356
446
 
357
- * 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.
358
- * Improve and validate requirements for Host and Path rewrite rules
359
- * 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.