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 +4 -4
- data/CHANGELOG.md +172 -0
- data/LICENSE +1 -1
- data/README.md +136 -59
- data/SECURITY.md +52 -0
- data/lib/rack/http_streaming_response.rb +137 -17
- data/lib/rack/proxy/version.rb +7 -0
- data/lib/rack/proxy.rb +239 -63
- data/lib/rack-proxy.rb +3 -1
- data/rack-proxy.gemspec +20 -16
- metadata +24 -27
- data/.github/FUNDING.yml +0 -3
- data/.gitignore +0 -3
- data/.travis.yml +0 -18
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -31
- data/Rakefile +0 -14
- data/lib/net_http_hacked.rb +0 -90
- data/lib/rack_proxy_examples/example_service_proxy.rb +0 -40
- data/lib/rack_proxy_examples/forward_host.rb +0 -24
- data/lib/rack_proxy_examples/rack_php_proxy.rb +0 -37
- data/lib/rack_proxy_examples/trusting_proxy.rb +0 -25
- data/test/http_streaming_response_test.rb +0 -50
- data/test/net_http_hacked_test.rb +0 -36
- data/test/rack_proxy_test.rb +0 -358
- data/test/test_helper.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e04f14fa6327e4f329211d6ca95a1865b2e2db929c954c613cd3017f2d9a587
|
|
4
|
+
data.tar.gz: ca3a3bbc4451fbcc21495317889bdb2afc2b5c328e8a0b6561a82617f3871c0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
|
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', '~>
|
|
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
|
-
* `:
|
|
41
|
-
* `:
|
|
42
|
-
* `:
|
|
43
|
-
* `:
|
|
44
|
-
* `:
|
|
45
|
-
* `:
|
|
46
|
-
* `:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
194
|
+
# a. `gem 'rack-proxy', '~> 1.0'`
|
|
136
195
|
# 4. install gem: `bundle install`
|
|
137
|
-
# 5.
|
|
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[
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
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"] ||=
|
|
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[
|
|
247
|
+
env["PATH_INFO"] = ENV["PHP_PATH"] || "/php/#{request.fullpath}"
|
|
190
248
|
|
|
191
|
-
env[
|
|
249
|
+
env["content-length"] = nil
|
|
192
250
|
|
|
193
|
-
super
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|