rack-proxy 0.8.3 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47505271ab54cee7d324c7fdd22c22ba7edae1b99146ee081913668925ff61f4
4
- data.tar.gz: 74afa4cf47b33eba1e12a8a4b5f8b499237fa6a6fabacbb46c0bc820e627b07b
3
+ metadata.gz: 1e04f14fa6327e4f329211d6ca95a1865b2e2db929c954c613cd3017f2d9a587
4
+ data.tar.gz: ca3a3bbc4451fbcc21495317889bdb2afc2b5c328e8a0b6561a82617f3871c0c
5
5
  SHA512:
6
- metadata.gz: 0ea871048c9cd7ceff7b13c7e59caad7d393947a9909e5372031cc51c85879d2e297b57c6245ea56dc7c104da3c5ed3bc5688e2ce7a8c006f68cba52c0ee1696
7
- data.tar.gz: 1c3ee10747af4bc06e9a270ec21d1854f1112b82127940a198e075ee044c7be6e6c43847b83d5cfd6facfadd90a32e4260a2e8731665ced34d46ec7ffd2febc1
6
+ metadata.gz: 37d457fb14f1cd3f48729b36db119b5d72585e85cad6f745d37c8a6857bddd6636e2ed8ad18f6d1975b61b184eb0cfb4b262d80b3304dfeebaa765c6bb273871
7
+ data.tar.gz: 1e2f04812c516b6ecaf1266b941bca1797fd72546217e506443b8e8a619617d77130684250ea343c19e90a686185a8f6cfd659970b9393d4378e7307bf26af2b
data/CHANGELOG.md ADDED
@@ -0,0 +1,172 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims to
5
+ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ Nothing yet.
10
+
11
+ ## [1.0.0] - 2026-07-18
12
+
13
+ The 2026 modernization + security-hardening release (see
14
+ `MODERNIZATION_PLAN.md`). From 1.0.0 on this project follows SemVer strictly:
15
+ breaking changes only in majors.
16
+
17
+ ### Breaking changes
18
+
19
+ Read this list before upgrading from 0.8.x; everything else below is additive
20
+ or a compatible fix. See the README's "Upgrading" section for migration steps.
21
+
22
+ - **Host-derived (dynamic) backends are refused by default.** With no
23
+ `:backend` and no `env["rack.backend"]`, requests now get `502` unless you
24
+ pass `allow_dynamic_backend: true`. A bare `Rack::Proxy.new` is no longer an
25
+ open proxy. Combine the opt-in with a `backend_allowed?` allowlist.
26
+ - **`net_http_hacked` is gone** — the file, `require "net_http_hacked"`, and
27
+ the `begin_request_hacked`/`end_request_hacked` methods (see Removed).
28
+ - **The bundled examples left the gem** — `require "rack_proxy_examples/..."`
29
+ raises `LoadError`; copy the snippets from `examples/` instead (see Removed).
30
+ - **Backend failures no longer raise.** `OpenSSL::SSL::SSLError`, `EOFError`,
31
+ timeouts, resets, and malformed backend responses now become a `502` triplet
32
+ instead of an exception — `rescue`-based error handling around `proxy.call`
33
+ must inspect the status instead.
34
+ - **Hop-by-hop request headers are no longer forwarded** (`Connection`, `TE`,
35
+ `Transfer-Encoding`, `Proxy-Authorization`, `Upgrade`, anything named by
36
+ `Connection`).
37
+ - **gzip backend bodies are forwarded verbatim in `streaming: false` mode**
38
+ (previously they were transparently inflated); `rewrite_response` hooks that
39
+ read body text must inflate it themselves.
40
+ - **Supported runtimes: Ruby >= 3.0 and Rack >= 2.0, < 4** (was Ruby >= 2.6,
41
+ rack unpinned).
42
+ - Smaller wire/API changes: body-less POST/PUT sends `Content-Length: 0` on
43
+ the streaming path; the streaming body is thread-affine (iterate it on the
44
+ thread that called the app, as mainstream servers do); `HttpStreamingResponse`
45
+ raises `IOError` on use-after-close; early termination closes the backend
46
+ connection instead of draining it.
47
+
48
+ ### Security
49
+
50
+ - Refuse Host-derived backends unless `allow_dynamic_backend: true` is set
51
+ (see Breaking changes) — closes the default open-proxy/SSRF pivot.
52
+ - Strip hop-by-hop headers from the **forwarded request** (Connection, TE,
53
+ Transfer-Encoding, Proxy-Authorization, …, plus any header named by the inbound
54
+ `Connection` header), closing a Content-Length/Transfer-Encoding request-smuggling
55
+ surface. Hop-by-hop headers were previously stripped only from the response.
56
+ - Add `backend_allowed?(backend)` — an overridable per-request allowlist hook,
57
+ consulted for every request (static backends included). A refused backend
58
+ responds `502`.
59
+ - Add `:max_response_length` to cap the backend response size (bounds memory
60
+ against a hostile/huge backend). Enforced incrementally while streaming.
61
+ - Add `:ca_file` / `:cert_store` so private-CA backends can be verified under the
62
+ default `VERIFY_PEER` instead of disabling verification.
63
+ - Add `:open_timeout` / `:write_timeout` to bound connect and per-write stalls
64
+ (previously only `:read_timeout` was configurable).
65
+
66
+ ### Added
67
+
68
+ - `:min_version` / `:max_version` TLS options (mapping to `Net::HTTP#min_version=`
69
+ / `#max_version=`). `:ssl_version` still works but is deprecated (it pins an
70
+ exact protocol and forbids TLS 1.3).
71
+ - `HttpStreamingResponse#close` so Rack servers release the backend connection on
72
+ early termination (HEAD, 304, client disconnect) instead of leaking it until GC.
73
+ - Opt-in request hardening: `strip_credentials: true` drops the client's
74
+ `Cookie`/`Authorization` headers from the forwarded request, and
75
+ `replace_x_forwarded_for: true` forwards only this hop's `REMOTE_ADDR`
76
+ instead of appending to the client-supplied `X-Forwarded-For` chain.
77
+ - Project scaffolding: `SECURITY.md`, `CHANGELOG.md`, `CONTRIBUTING.md`,
78
+ `CLAUDE.md`/`AGENTS.md`, GitHub Actions CI (Ruby 3.1–3.4 × Rack 2/3),
79
+ Dependabot, issue/PR templates, SimpleCov with a ratcheted coverage floor
80
+ (`COVERAGE=1`), and Standard (`standardrb`) + `bundler-audit` enforced by a
81
+ CI lint job.
82
+
83
+ ### Changed
84
+
85
+ - **The streaming path no longer monkey-patches `net/http`.**
86
+ `Rack::HttpStreamingResponse` now runs the public block form of
87
+ `Net::HTTP#request` inside a Fiber (pausing at the response head, resuming
88
+ per body chunk), replacing the 2010-era patch of private `net/http` internals
89
+ in `net_http_hacked.rb`. Behavioral notes: the streaming session sets
90
+ `max_retries = 0` so a transport error can never silently replay the request
91
+ mid-stream; early termination (client abort, HEAD, 204/304) now closes
92
+ the backend connection immediately instead of draining the remaining body;
93
+ and a body-less POST/PUT/PATCH on the streaming path now carries
94
+ `Content-Length: 0` (matching the non-streaming path and plain `Net::HTTP` —
95
+ the old patched path sent no `Content-Length` at all).
96
+ - Backend and construction failures now map to status codes instead of raising a
97
+ `500`: `400` (malformed request URI), `501` (unknown HTTP method), `502`
98
+ (broadened backend-error set incl. `ECONNRESET`, `EPIPE`, read/write timeouts,
99
+ `EOFError`, `OpenSSL::SSL::SSLError`, protocol errors, and malformed backend
100
+ responses — `Net::HTTPBadResponse` / `Net::HTTPHeaderSyntaxError`).
101
+ - gzip-encoded backend responses are forwarded verbatim (Content-Encoding and
102
+ Content-Length preserved) instead of being transparently inflated.
103
+ - Skip all `1xx` interim responses (including `103 Early Hints`) on the streaming
104
+ path, so a backend's `103` is no longer mistaken for the final response.
105
+ - `rack` dependency constrained to `>= 2.0, < 4`; the library now `require`s rack
106
+ itself.
107
+ - Test suite is fully offline (local WEBrick server); the previous live-host
108
+ tests are gated behind `LIVE=1`.
109
+
110
+ ### Deprecated
111
+
112
+ - `:ssl_version` — use `:min_version` / `:max_version`.
113
+
114
+ ### Removed
115
+
116
+ - `lib/net_http_hacked.rb` — the 2010-era monkey-patch of private `Net::HTTP`
117
+ internals. The library stopped using it when streaming moved to the public
118
+ `Net::HTTP` API (see Changed); `require "net_http_hacked"` and the
119
+ `begin_request_hacked` / `end_request_hacked` methods are gone. If external
120
+ code still depends on them, vendor the file from a 0.8.x release — and plan
121
+ to migrate; it breaks under modern net/http refactors.
122
+
123
+ - The bundled examples moved from the gem load path
124
+ (`lib/rack_proxy_examples/`) to [`examples/`](examples/) in the repository.
125
+ `require "rack_proxy_examples/..."` no longer works — copy the example class
126
+ into your app instead (they were never safe to require blindly: each one
127
+ installs itself into the Rails middleware stack when Rails is booted).
128
+
129
+ ## [0.8.3] - 2025
130
+
131
+ ### Fixed
132
+
133
+ - Handle non-rewindable request body streams (Rack 3 input streams need not
134
+ respond to `#rewind`). (#128)
135
+
136
+ ## [0.8.2] - 2025
137
+
138
+ ### Fixed
139
+
140
+ - Harden `build_header_hash` against a top-level `::Headers` constant defined by
141
+ the host app being picked up instead of `Rack::Headers`.
142
+
143
+ ## [0.8.1] - 2025
144
+
145
+ ### Added
146
+
147
+ - `:logger` option, wired to `Net::HTTP#set_debug_output` for wire-level debug
148
+ output. (#80)
149
+
150
+ ## [0.8.0] - 2025
151
+
152
+ ### Changed
153
+
154
+ - **BREAKING:** TLS certificate verification now defaults to `VERIFY_PEER`
155
+ (Ruby's `Net::HTTP` default). Previous versions silently used `VERIFY_NONE` for
156
+ HTTPS backends. Pass `ssl_verify_none: true` (or `verify_mode:`) to opt out. (#113)
157
+
158
+ ### Fixed
159
+
160
+ - Return `502` on backend connection errors instead of raising; correct body
161
+ handling for empty responses and no-entity-body statuses (1xx/204/304). (#58, #122, #123)
162
+
163
+ ---
164
+
165
+ Older releases (≤ 0.7.8) predate this changelog; see the git history and tags.
166
+
167
+ [Unreleased]: https://github.com/ncr/rack-proxy/compare/v1.0.0...HEAD
168
+ [1.0.0]: https://github.com/ncr/rack-proxy/compare/v0.8.3...v1.0.0
169
+ [0.8.3]: https://github.com/ncr/rack-proxy/compare/v0.8.2...v0.8.3
170
+ [0.8.2]: https://github.com/ncr/rack-proxy/compare/v0.8.1...v0.8.2
171
+ [0.8.1]: https://github.com/ncr/rack-proxy/compare/v0.8.0...v0.8.1
172
+ [0.8.0]: https://github.com/ncr/rack-proxy/compare/v0.7.8...v0.8.0
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2013 Jacek Becela jacek.becela@gmail.com
3
+ Copyright (c) 2010-2026 Jacek Becela and contributors jacek.becela@gmail.com
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,12 +1,25 @@
1
+ # Rack::Proxy
2
+
1
3
  A request/response rewriting HTTP proxy. A Rack app. Subclass `Rack::Proxy` and provide your `rewrite_env` and `rewrite_response` methods.
2
4
 
5
+ ## Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Use Cases](#use-cases)
9
+ - [Options](#options)
10
+ - [Security considerations](#security-considerations)
11
+ - [Examples](#examples)
12
+ - [Upgrading](#upgrading)
13
+ - [A note on header keys](#a-note-on-header-keys-96)
14
+ - [Compatibility notes](#compatibility-notes)
15
+
3
16
  Installation
4
17
  ----
5
18
 
6
19
  Add the following to your `Gemfile`:
7
20
 
8
21
  ```
9
- gem 'rack-proxy', '~> 0.8.0'
22
+ gem 'rack-proxy', '~> 1.0'
10
23
  ```
11
24
 
12
25
  Or install:
@@ -36,14 +49,29 @@ Options
36
49
 
37
50
  Options can be set when initializing the middleware or overriding a method.
38
51
 
39
-
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.
52
+ * `: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)).
53
+ * `: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` and [Security considerations](#security-considerations).
54
+ * `: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.
55
+ * `:read_timeout` - per-read timeout in seconds (default `60`).
56
+ * `:open_timeout` - connection-open timeout in seconds.
57
+ * `:write_timeout` - per-write timeout in seconds.
58
+ * `:ssl_verify_none` - skip TLS certificate verification. Verification is on by default (`VERIFY_PEER`) — see [Upgrading](#upgrading).
59
+ * `:verify_mode` - explicit `OpenSSL::SSL::VERIFY_*` constant; wins over `ssl_verify_none`.
60
+ * `:ca_file` - path to a PEM CA bundle used to verify the backend certificate (prefer this over disabling verification for private CAs).
61
+ * `:cert_store` - an `OpenSSL::X509::Store` used to verify the backend certificate.
62
+ * `:cert` / `:key` - client certificate and key for mutual TLS to the backend.
63
+ * `:min_version` / `:max_version` - TLS protocol range (e.g. `:TLS1_2`), mapped to `Net::HTTP#min_version=` / `#max_version=`.
64
+ * `:ssl_version` - **deprecated**; pins an exact protocol (forbids TLS 1.3). Use `:min_version` / `:max_version`.
65
+ * `: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).
66
+ * `:username` / `:password` - HTTP Basic credentials sent to the backend.
67
+ * `: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.
68
+ * `: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).
69
+ * `: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. Useful for debugging.
70
+
71
+ Two request-scoped overrides can also be set in `env` (e.g. from `rewrite_env`):
72
+
73
+ * `env['rack.backend']` - a URI overriding `:backend` for this request.
74
+ * `env['http.read_timeout']` - override `:read_timeout` for this request.
47
75
 
48
76
  To pass in options, when you configure your middleware you can pass them in as an optional hash.
49
77
 
@@ -51,31 +79,62 @@ To pass in options, when you configure your middleware you can pass them in as a
51
79
  Rails.application.config.middleware.use ExampleServiceProxy, backend: 'http://guides.rubyonrails.org', streaming: false
52
80
  ```
53
81
 
82
+ Security considerations
83
+ ----
84
+
85
+ rack-proxy forwards attacker-influenced requests to a backend and relays the backend's response. Configure and subclass it with that in mind.
86
+
87
+ * **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):
88
+
89
+ ```ruby
90
+ class MyProxy < Rack::Proxy
91
+ ALLOWED = %w[api.internal.example.com].freeze
92
+
93
+ def backend_allowed?(backend)
94
+ ALLOWED.include?(backend.host)
95
+ end
96
+ end
97
+
98
+ MyProxy.new(allow_dynamic_backend: true)
99
+ ```
100
+
101
+ A refused backend is answered with `502` (with a hint in the `:logger` output).
102
+
103
+ * **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.
104
+
105
+ * **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.
106
+
107
+ * **TLS verification** defaults to `VERIFY_PEER`. For private-CA backends use `:ca_file` / `:cert_store` rather than `ssl_verify_none: true`.
108
+
109
+ * **Resource limits.** Use `:max_response_length` plus `:open_timeout` / `:write_timeout` / `:read_timeout` to bound memory and stalls against a hostile or slow backend.
110
+
111
+ * **Hop-by-hop headers** (Connection, TE, Transfer-Encoding, Proxy-Authorization, …) are stripped from both the forwarded request and the response.
112
+
113
+ To report a vulnerability, see [SECURITY.md](SECURITY.md).
114
+
54
115
  Examples
55
116
  ----
56
117
 
57
- See and run the examples below from `lib/rack_proxy_examples/`. To mount any example into an existing Rails app:
118
+ 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. To mount one in Rails, copy the class into your app and add it to the middleware stack in an initializer:
58
119
 
59
- 1. create `config/initializers/proxy.rb`
60
- 2. modify the file to require the example file
61
120
  ```ruby
62
- require 'rack_proxy_examples/forward_host'
121
+ # config/initializers/proxy.rb
122
+ Rails.application.config.middleware.use ForwardHost, backend: "http://example.com"
63
123
  ```
64
124
 
65
125
  ### Forward request to Host and Insert Header
66
126
 
67
- Test with `require 'rack_proxy_examples/forward_host'`
127
+ From [`examples/forward_host.rb`](examples/forward_host.rb):
68
128
 
69
129
  ```ruby
70
130
  class ForwardHost < Rack::Proxy
71
-
72
131
  def rewrite_env(env)
73
132
  env["HTTP_HOST"] = "example.com"
74
133
  env
75
134
  end
76
135
 
77
136
  def rewrite_response(triplet)
78
- status, headers, body = triplet
137
+ _, headers, _ = triplet
79
138
 
80
139
  # example of inserting an additional header
81
140
  headers["X-Foo"] = "Bar"
@@ -87,24 +146,22 @@ class ForwardHost < Rack::Proxy
87
146
 
88
147
  triplet
89
148
  end
90
-
91
149
  end
92
150
  ```
93
151
 
94
152
  ### Disable SSL session verification when proxying a server with e.g. self-signed SSL certs
95
153
 
96
- Test with `require 'rack_proxy_examples/trusting_proxy'`
154
+ From [`examples/trusting_proxy.rb`](examples/trusting_proxy.rb):
97
155
 
98
156
  ```ruby
99
157
  class TrustingProxy < Rack::Proxy
100
-
101
158
  def rewrite_env(env)
102
159
  env["HTTP_HOST"] = "self-signed.badssl.com"
103
160
  env
104
161
  end
105
162
 
106
163
  def rewrite_response(triplet)
107
- status, headers, body = triplet
164
+ _, headers, _ = triplet
108
165
 
109
166
  # if you rewrite env, it appears that content-length isn't calculated correctly
110
167
  # resulting in only partial responses being sent to users
@@ -113,16 +170,18 @@ class TrustingProxy < Rack::Proxy
113
170
 
114
171
  triplet
115
172
  end
116
-
117
173
  end
118
174
 
119
- # Pass ssl_verify_none: true to skip TLS certificate verification.
120
- Rack::Proxy.new(ssl_verify_none: true)
175
+ # Mount it with an explicit backend (dynamic Host-derived backends are refused
176
+ # by default since 1.0). Pass ssl_verify_none: true to skip TLS verification.
177
+ Rails.application.config.middleware.use TrustingProxy,
178
+ backend: "https://self-signed.badssl.com",
179
+ ssl_verify_none: true
121
180
  ```
122
181
 
123
182
  ### Rails middleware example
124
183
 
125
- Test with `require 'rack_proxy_examples/example_service_proxy'`
184
+ From [`examples/example_service_proxy.rb`](examples/example_service_proxy.rb):
126
185
 
127
186
  ```ruby
128
187
  ###
@@ -132,32 +191,32 @@ Test with `require 'rack_proxy_examples/example_service_proxy'`
132
191
  # 1. rails new test_app
133
192
  # 2. cd test_app
134
193
  # 3. install Rack-Proxy in `Gemfile`
135
- # a. `gem 'rack-proxy', '~> 0.7.7'`
194
+ # a. `gem 'rack-proxy', '~> 1.0'`
136
195
  # 4. install gem: `bundle install`
137
- # 5. create `config/initializers/proxy.rb` adding this line `require 'rack_proxy_examples/example_service_proxy'`
196
+ # 5. copy the class into your app and mount it from `config/initializers/proxy.rb`
138
197
  # 6. run: `SERVICE_URL=http://guides.rubyonrails.org rails server`
139
198
  # 7. open in browser: `http://localhost:3000/example_service`
140
199
  #
141
200
  ###
142
- ENV['SERVICE_URL'] ||= 'http://guides.rubyonrails.org'
201
+ ENV["SERVICE_URL"] ||= "http://guides.rubyonrails.org"
143
202
 
144
203
  class ExampleServiceProxy < Rack::Proxy
145
204
  def perform_request(env)
146
205
  request = Rack::Request.new(env)
147
206
 
148
207
  # 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)
208
+ if %r{^/example_service}.match?(request.path)
209
+ backend = URI(ENV["SERVICE_URL"])
210
+ # most backends required host set properly, but rack-proxy doesn't set this for you automatically
211
+ # even when a backend host is passed in via the options
212
+ env["HTTP_HOST"] = backend.host
213
+
214
+ # This is the only path that needs to be set currently on Rails 5 & greater
215
+ env["PATH_INFO"] = ENV["SERVICE_PATH"] || "/configuring.html"
216
+
217
+ # don't send your sites cookies to target service, unless it is a trusted internal service that can parse all your cookies
218
+ env["HTTP_COOKIE"] = ""
219
+ super
161
220
  else
162
221
  @app.call(env)
163
222
  end
@@ -167,7 +226,7 @@ end
167
226
 
168
227
  ### Using as middleware to forward only some extensions to another Application
169
228
 
170
- Test with `require 'rack_proxy_examples/rack_php_proxy'`
229
+ From [`examples/rack_php_proxy.rb`](examples/rack_php_proxy.rb):
171
230
 
172
231
  Example: Proxying only requests that end with ".php" could be done like this:
173
232
 
@@ -176,28 +235,27 @@ Example: Proxying only requests that end with ".php" could be done like this:
176
235
  # Open http://localhost:3000/test.php to trigger proxy
177
236
  ###
178
237
  class RackPhpProxy < Rack::Proxy
179
-
180
238
  def perform_request(env)
181
239
  request = Rack::Request.new(env)
182
- if request.path =~ %r{\.php}
240
+ if %r{\.php}.match?(request.path)
183
241
  env["HTTP_HOST"] = ENV["HTTP_HOST"] ? URI(ENV["HTTP_HOST"]).host : "localhost"
184
- ENV["PHP_PATH"] ||= '/manual/en/tutorial.firstpage.php'
242
+ ENV["PHP_PATH"] ||= "/manual/en/tutorial.firstpage.php"
185
243
 
186
244
  # Rails 3 & 4
187
245
  env["REQUEST_PATH"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
188
246
  # Rails 5 and above
189
- env['PATH_INFO'] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
247
+ env["PATH_INFO"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
190
248
 
191
- env['content-length'] = nil
249
+ env["content-length"] = nil
192
250
 
193
- super(env)
251
+ super
194
252
  else
195
253
  @app.call(env)
196
254
  end
197
255
  end
198
256
 
199
257
  def rewrite_response(triplet)
200
- status, headers, body = triplet
258
+ _, headers, _ = triplet
201
259
 
202
260
  # if you proxy depending on the backend, it appears that content-length isn't calculated correctly
203
261
  # resulting in only partial responses being sent to users
@@ -214,14 +272,14 @@ To use the middleware, please consider the following:
214
272
  1) For Rails we could add a configuration in `config/application.rb`
215
273
 
216
274
  ```ruby
217
- config.middleware.use RackPhpProxy, {ssl_verify_none: true}
275
+ config.middleware.use RackPhpProxy, backend: "http://php.net", ssl_verify_none: true
218
276
  ```
219
277
 
220
278
  2) For Sinatra or any Rack-based application:
221
279
 
222
280
  ```ruby
223
281
  class MyAwesomeSinatra < Sinatra::Base
224
- use RackPhpProxy, {ssl_verify_none: true}
282
+ use RackPhpProxy, backend: "http://php.net", ssl_verify_none: true
225
283
  end
226
284
  ```
227
285
 
@@ -307,7 +365,7 @@ key_raw = File.read('./certs/key.pem')
307
365
  cert = OpenSSL::X509::Certificate.new(cert_raw)
308
366
  key = OpenSSL::PKey.read(key_raw)
309
367
 
310
- use TLSProxy, cert: cert, key: key, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER, ssl_version: 'TLSv1_2'
368
+ use TLSProxy, backend: "https://client-tls-auth-api.com", cert: cert, key: key, verify_mode: OpenSSL::SSL::VERIFY_PEER, min_version: :TLS1_2
311
369
  ```
312
370
 
313
371
  And rewrite host for example:
@@ -326,6 +384,32 @@ end
326
384
  Upgrading
327
385
  ----
328
386
 
387
+ ### 0.8.x → 1.0.0
388
+
389
+ 1.0.0 is a breaking release; the full list is in [CHANGELOG.md](CHANGELOG.md). The changes most likely to need action:
390
+
391
+ **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:
392
+
393
+ ```ruby
394
+ class MyProxy < Rack::Proxy
395
+ def backend_allowed?(backend)
396
+ %w[api.internal.example.com].include?(backend.host)
397
+ end
398
+ end
399
+
400
+ MyProxy.new(allow_dynamic_backend: true)
401
+ ```
402
+
403
+ Deployments with a fixed `:backend` (or that set `env["rack.backend"]` in `rewrite_env`) need no change.
404
+
405
+ **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`.
406
+
407
+ **`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.
408
+
409
+ **`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.
410
+
411
+ **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.
412
+
329
413
  ### 0.7.x → 0.8.0
330
414
 
331
415
  **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`.
@@ -346,14 +430,7 @@ Per the standard Rack/CGI convention, header names received by your proxy are ex
346
430
 
347
431
  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
432
 
349
- WARNING
350
- ----
351
-
352
- Doesn't work with `fakeweb`/`webmock`. Both libraries monkey-patch net/http code.
353
-
354
- Todos
433
+ Compatibility notes
355
434
  ----
356
435
 
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
436
+ 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`.
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.