rack-proxy 0.7.8 → 0.8.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/Gemfile.lock +4 -2
- data/README.md +27 -9
- data/lib/rack/proxy.rb +36 -26
- data/lib/rack_proxy_examples/trusting_proxy.rb +6 -5
- data/rack-proxy.gemspec +1 -0
- data/test/rack_proxy_test.rb +142 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9520dd879490c3e4e00d0617a74994fd178488878cf149f44acc4795975609e
|
|
4
|
+
data.tar.gz: 99c9ea6a98cbae6713c03f4ac4a6b87cf682a9189c073f29ecd5845d3ddc5c7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5b53e01fef046f99405006fcb5b60f053ede10fc23bb231ad27deebbe52593d1ad1fcd1ba77471dcb76405c65a9d7a3a685ed13caea6bdb1dbf927ad19ffffc
|
|
7
|
+
data.tar.gz: 940b3f2ba7a1248475799497e757a9a9be8875e933ef47b0fdc583181d0368804c56d0bd5b356860c169e4bdd9e68f2cfd6b7c6f273b9d542fd5bdd93b6e7f46
|
data/Gemfile.lock
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rack-proxy (0.
|
|
4
|
+
rack-proxy (0.8.0)
|
|
5
5
|
rack
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
10
|
power_assert (2.0.3)
|
|
11
|
-
rack (3.
|
|
11
|
+
rack (3.2.6)
|
|
12
12
|
rack-test (2.1.0)
|
|
13
13
|
rack (>= 1.3)
|
|
14
14
|
rake (13.0.6)
|
|
15
15
|
test-unit (3.6.1)
|
|
16
16
|
power_assert
|
|
17
|
+
webrick (1.9.2)
|
|
17
18
|
|
|
18
19
|
PLATFORMS
|
|
19
20
|
arm64-darwin-22
|
|
@@ -24,6 +25,7 @@ DEPENDENCIES
|
|
|
24
25
|
rack-test
|
|
25
26
|
rake
|
|
26
27
|
test-unit
|
|
28
|
+
webrick
|
|
27
29
|
|
|
28
30
|
BUNDLED WITH
|
|
29
31
|
2.4.17
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Installation
|
|
|
6
6
|
Add the following to your `Gemfile`:
|
|
7
7
|
|
|
8
8
|
```
|
|
9
|
-
gem 'rack-proxy', '~> 0.
|
|
9
|
+
gem 'rack-proxy', '~> 0.8.0'
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Or install:
|
|
@@ -38,7 +38,8 @@ Options can be set when initializing the middleware or overriding a method.
|
|
|
38
38
|
|
|
39
39
|
|
|
40
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
|
|
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`
|
|
42
43
|
* `:ssl_version` - tell `Net::HTTP` to set a specific `ssl_version`
|
|
43
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.
|
|
44
45
|
* `:read_timeout` - set proxy timeout it defaults to 60 seconds
|
|
@@ -98,9 +99,6 @@ class TrustingProxy < Rack::Proxy
|
|
|
98
99
|
|
|
99
100
|
def rewrite_env(env)
|
|
100
101
|
env["HTTP_HOST"] = "self-signed.badssl.com"
|
|
101
|
-
|
|
102
|
-
# We are going to trust the self-signed SSL
|
|
103
|
-
env["rack.ssl_verify_none"] = true
|
|
104
102
|
env
|
|
105
103
|
end
|
|
106
104
|
|
|
@@ -116,11 +114,8 @@ class TrustingProxy < Rack::Proxy
|
|
|
116
114
|
end
|
|
117
115
|
|
|
118
116
|
end
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
The same can be achieved for *all* requests going through the `Rack::Proxy` instance by using
|
|
122
117
|
|
|
123
|
-
|
|
118
|
+
# Pass ssl_verify_none: true to skip TLS certificate verification.
|
|
124
119
|
Rack::Proxy.new(ssl_verify_none: true)
|
|
125
120
|
```
|
|
126
121
|
|
|
@@ -327,6 +322,29 @@ class TLSProxy < Rack::Proxy
|
|
|
327
322
|
end
|
|
328
323
|
```
|
|
329
324
|
|
|
325
|
+
Upgrading
|
|
326
|
+
----
|
|
327
|
+
|
|
328
|
+
### 0.7.x → 0.8.0
|
|
329
|
+
|
|
330
|
+
**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`.
|
|
331
|
+
|
|
332
|
+
If you proxy to a backend with a self-signed or otherwise untrusted certificate, you'll now get an `OpenSSL::SSL::SSLError` unless you opt out explicitly:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
Rack::Proxy.new(ssl_verify_none: true) # or
|
|
336
|
+
Rack::Proxy.new(verify_mode: OpenSSL::SSL::VERIFY_NONE)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
For internal services with a private CA, prefer setting `cert`/`verify_mode` over disabling verification altogether.
|
|
340
|
+
|
|
341
|
+
A note on header keys (#96)
|
|
342
|
+
----
|
|
343
|
+
|
|
344
|
+
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.
|
|
345
|
+
|
|
346
|
+
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.
|
|
347
|
+
|
|
330
348
|
WARNING
|
|
331
349
|
----
|
|
332
350
|
|
data/lib/rack/proxy.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Rack
|
|
|
5
5
|
|
|
6
6
|
# Subclass and bring your own #rewrite_request and #rewrite_response
|
|
7
7
|
class Proxy
|
|
8
|
-
VERSION = "0.
|
|
8
|
+
VERSION = "0.8.0".freeze
|
|
9
9
|
|
|
10
10
|
HOP_BY_HOP_HEADERS = {
|
|
11
11
|
'connection' => true,
|
|
@@ -69,13 +69,16 @@ module Rack
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
@streaming = opts.fetch(:streaming, true)
|
|
72
|
-
@ssl_verify_none = opts.fetch(:ssl_verify_none, false)
|
|
73
72
|
@backend = opts[:backend] ? URI(opts[:backend]) : nil
|
|
74
73
|
@read_timeout = opts.fetch(:read_timeout, 60)
|
|
75
74
|
@ssl_version = opts[:ssl_version]
|
|
76
75
|
@cert = opts[:cert]
|
|
77
76
|
@key = opts[:key]
|
|
77
|
+
# SSL verification: defaults to VERIFY_PEER (Ruby's Net::HTTP default).
|
|
78
|
+
# Pass ssl_verify_none: true to explicitly disable cert verification, or
|
|
79
|
+
# pass verify_mode: <OpenSSL::SSL::VERIFY_*> for full control.
|
|
78
80
|
@verify_mode = opts[:verify_mode]
|
|
81
|
+
@verify_mode ||= OpenSSL::SSL::VERIFY_NONE if opts[:ssl_verify_none]
|
|
79
82
|
|
|
80
83
|
@username = opts[:username]
|
|
81
84
|
@password = opts[:password]
|
|
@@ -130,33 +133,40 @@ module Rack
|
|
|
130
133
|
read_timeout = env.delete('http.read_timeout') || @read_timeout
|
|
131
134
|
|
|
132
135
|
# Create the response
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
http.
|
|
136
|
+
begin
|
|
137
|
+
if @streaming
|
|
138
|
+
# streaming response (the actual network communication is deferred, a.k.a. streamed)
|
|
139
|
+
target_response = HttpStreamingResponse.new(target_request, backend.host, backend.port)
|
|
140
|
+
target_response.use_ssl = use_ssl
|
|
141
|
+
target_response.read_timeout = read_timeout
|
|
142
|
+
target_response.ssl_version = @ssl_version if @ssl_version
|
|
143
|
+
target_response.verify_mode = (@verify_mode || OpenSSL::SSL::VERIFY_PEER) if use_ssl
|
|
144
|
+
target_response.cert = @cert if @cert
|
|
145
|
+
target_response.key = @key if @key
|
|
146
|
+
else
|
|
147
|
+
http = Net::HTTP.new(backend.host, backend.port)
|
|
148
|
+
http.use_ssl = use_ssl if use_ssl
|
|
149
|
+
http.read_timeout = read_timeout
|
|
150
|
+
http.ssl_version = @ssl_version if @ssl_version
|
|
151
|
+
http.verify_mode = @verify_mode || OpenSSL::SSL::VERIFY_PEER if use_ssl
|
|
152
|
+
http.cert = @cert if @cert
|
|
153
|
+
http.key = @key if @key
|
|
154
|
+
|
|
155
|
+
target_response = http.start do
|
|
156
|
+
http.request(target_request)
|
|
157
|
+
end
|
|
153
158
|
end
|
|
159
|
+
|
|
160
|
+
code = target_response.code
|
|
161
|
+
headers = self.class.normalize_headers(target_response.respond_to?(:headers) ? target_response.headers : target_response.to_hash)
|
|
162
|
+
body = target_response.body || []
|
|
163
|
+
body = [body] unless body.respond_to?(:each)
|
|
164
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT, Net::OpenTimeout, SocketError
|
|
165
|
+
return [502, {}, []]
|
|
154
166
|
end
|
|
155
167
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
body = target_response.body || [""]
|
|
159
|
-
body = [body] unless body.respond_to?(:each)
|
|
168
|
+
# No entity body for status codes that don't allow one (1xx, 204, 304)
|
|
169
|
+
body = [] if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[code.to_i]
|
|
160
170
|
|
|
161
171
|
# According to https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3.1Acc
|
|
162
172
|
# should remove hop-by-hop header fields
|
|
@@ -2,15 +2,12 @@ class TrustingProxy < Rack::Proxy
|
|
|
2
2
|
|
|
3
3
|
def rewrite_env(env)
|
|
4
4
|
env["HTTP_HOST"] = "self-signed.badssl.com"
|
|
5
|
-
|
|
6
|
-
# We are going to trust the self-signed SSL
|
|
7
|
-
env["rack.ssl_verify_none"] = true
|
|
8
5
|
env
|
|
9
6
|
end
|
|
10
7
|
|
|
11
8
|
def rewrite_response(triplet)
|
|
12
9
|
status, headers, body = triplet
|
|
13
|
-
|
|
10
|
+
|
|
14
11
|
# if you rewrite env, it appears that content-length isn't calculated correctly
|
|
15
12
|
# resulting in only partial responses being sent to users
|
|
16
13
|
# you can remove it or recalculate it here
|
|
@@ -21,4 +18,8 @@ class TrustingProxy < Rack::Proxy
|
|
|
21
18
|
|
|
22
19
|
end
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
# Pass ssl_verify_none: true to skip TLS certificate verification.
|
|
22
|
+
Rails.application.config.middleware.use TrustingProxy,
|
|
23
|
+
backend: 'https://self-signed.badssl.com',
|
|
24
|
+
streaming: false,
|
|
25
|
+
ssl_verify_none: true
|
data/rack-proxy.gemspec
CHANGED
data/test/rack_proxy_test.rb
CHANGED
|
@@ -125,4 +125,146 @@ class RackProxyTest < Test::Unit::TestCase
|
|
|
125
125
|
get 'https://example.com/oauth2/token/info?access_token=123'
|
|
126
126
|
assert !last_response.headers.key?('transfer-encoding')
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
# Issue #58: connection errors should return 502, not raise.
|
|
130
|
+
def test_connection_refused_returns_502
|
|
131
|
+
# Bind a socket to find a free port, then close it so connection is refused.
|
|
132
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
133
|
+
closed_port = server.addr[1]
|
|
134
|
+
server.close
|
|
135
|
+
|
|
136
|
+
app({:streaming => false}).host = "127.0.0.1:#{closed_port}"
|
|
137
|
+
get '/'
|
|
138
|
+
assert_equal 502, last_response.status
|
|
139
|
+
assert_equal '', last_response.body
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_connection_refused_returns_502_streaming
|
|
143
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
144
|
+
closed_port = server.addr[1]
|
|
145
|
+
server.close
|
|
146
|
+
|
|
147
|
+
app({:streaming => true}).host = "127.0.0.1:#{closed_port}"
|
|
148
|
+
get '/'
|
|
149
|
+
assert_equal 502, last_response.status
|
|
150
|
+
assert_equal '', last_response.body
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_unknown_host_returns_502
|
|
154
|
+
app({:streaming => false}).host = 'no-such-host.invalid'
|
|
155
|
+
get '/'
|
|
156
|
+
assert_equal 502, last_response.status
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Issues #122/#123: body should be [] for empty responses and for status
|
|
160
|
+
# codes that don't allow an entity body (1xx, 204, 304).
|
|
161
|
+
def test_no_entity_body_for_204
|
|
162
|
+
with_webrick_proxy(streaming: false) do |port, proxy|
|
|
163
|
+
proxy.host = "127.0.0.1:#{port}"
|
|
164
|
+
get '/no-content'
|
|
165
|
+
assert_equal 204, last_response.status
|
|
166
|
+
assert_equal '', last_response.body
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_no_entity_body_for_304
|
|
171
|
+
with_webrick_proxy(streaming: false) do |port, proxy|
|
|
172
|
+
proxy.host = "127.0.0.1:#{port}"
|
|
173
|
+
get '/not-modified'
|
|
174
|
+
assert_equal 304, last_response.status
|
|
175
|
+
assert_equal '', last_response.body
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_empty_body_is_not_array_with_empty_string
|
|
180
|
+
with_webrick_proxy(streaming: false) do |port, proxy|
|
|
181
|
+
proxy.host = "127.0.0.1:#{port}"
|
|
182
|
+
get '/empty'
|
|
183
|
+
assert_equal 200, last_response.status
|
|
184
|
+
assert_equal '', last_response.body
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Issue #65: header values must be strings, not single-element arrays,
|
|
189
|
+
# for both streaming and non-streaming paths.
|
|
190
|
+
def test_header_values_are_strings_streaming
|
|
191
|
+
assert_no_array_header_values(streaming: true)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_header_values_are_strings_non_streaming
|
|
195
|
+
assert_no_array_header_values(streaming: false)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Issue #113: SSL cert verification must default to VERIFY_PEER (Ruby's
|
|
199
|
+
# Net::HTTP default), not VERIFY_NONE.
|
|
200
|
+
def test_ssl_default_is_verify_peer
|
|
201
|
+
proxy = Rack::Proxy.new
|
|
202
|
+
assert_nil proxy.instance_variable_get(:@verify_mode),
|
|
203
|
+
"@verify_mode should be unset by default so VERIFY_PEER applies at request time"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_ssl_verify_none_opt_in
|
|
207
|
+
proxy = Rack::Proxy.new(ssl_verify_none: true)
|
|
208
|
+
assert_equal OpenSSL::SSL::VERIFY_NONE, proxy.instance_variable_get(:@verify_mode)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_explicit_verify_mode_wins_over_ssl_verify_none
|
|
212
|
+
proxy = Rack::Proxy.new(ssl_verify_none: true, verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
|
213
|
+
assert_equal OpenSSL::SSL::VERIFY_PEER, proxy.instance_variable_get(:@verify_mode)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_https_default_rejects_invalid_certificate
|
|
217
|
+
# self-signed cert on a public test host should be rejected with the new default
|
|
218
|
+
app({:streaming => false}).host = 'self-signed.badssl.com'
|
|
219
|
+
error = assert_raise(OpenSSL::SSL::SSLError) { get 'https://example.com/' }
|
|
220
|
+
assert_match(/certificate verify failed/, error.message)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_https_with_ssl_verify_none_accepts_invalid_certificate
|
|
224
|
+
app({:streaming => false, :ssl_verify_none => true}).host = 'self-signed.badssl.com'
|
|
225
|
+
get 'https://example.com/'
|
|
226
|
+
assert last_response.ok?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def assert_no_array_header_values(streaming:)
|
|
232
|
+
with_webrick_proxy(streaming: streaming) do |port, proxy|
|
|
233
|
+
proxy.host = "127.0.0.1:#{port}"
|
|
234
|
+
get '/echo-headers'
|
|
235
|
+
array_valued = last_response.headers.select { |_, v| v.is_a?(Array) }
|
|
236
|
+
assert_empty array_valued,
|
|
237
|
+
"expected no Array-valued headers (#65), got: #{array_valued.inspect}"
|
|
238
|
+
assert_equal 'value-here', last_response['x-custom']
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Spin up a tiny WEBrick server with fixed routes so we can exercise the
|
|
244
|
+
# proxy against real Net::HTTP requests without depending on a remote host.
|
|
245
|
+
def with_webrick_proxy(streaming:)
|
|
246
|
+
require 'webrick'
|
|
247
|
+
server = WEBrick::HTTPServer.new(
|
|
248
|
+
Port: 0,
|
|
249
|
+
BindAddress: '127.0.0.1',
|
|
250
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
251
|
+
AccessLog: []
|
|
252
|
+
)
|
|
253
|
+
server.mount_proc('/no-content') { |_req, res| res.status = 204 }
|
|
254
|
+
server.mount_proc('/not-modified') { |_req, res| res.status = 304 }
|
|
255
|
+
server.mount_proc('/empty') { |_req, res| res.body = '' }
|
|
256
|
+
server.mount_proc('/echo-headers') do |_req, res|
|
|
257
|
+
res['x-custom'] = 'value-here'
|
|
258
|
+
res.body = 'ok'
|
|
259
|
+
end
|
|
260
|
+
Thread.new { server.start }
|
|
261
|
+
port = server.config[:Port]
|
|
262
|
+
|
|
263
|
+
proxy = HostProxy.new(streaming: streaming)
|
|
264
|
+
@app = proxy
|
|
265
|
+
yield port, proxy
|
|
266
|
+
ensure
|
|
267
|
+
server&.shutdown
|
|
268
|
+
@app = nil
|
|
269
|
+
end
|
|
128
270
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-proxy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jacek Becela
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webrick
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
54
68
|
description: A Rack app that provides request/response rewriting proxy capabilities
|
|
55
69
|
with streaming.
|
|
56
70
|
email:
|