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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f1a4b368973c939e40b8f152e168081d55105a2a447541d2ea8a2b9157aaf65
4
- data.tar.gz: f24595aef9a2aeaa8354f0377d1f28508f248039a59a880f1da674891e6236af
3
+ metadata.gz: f9520dd879490c3e4e00d0617a74994fd178488878cf149f44acc4795975609e
4
+ data.tar.gz: 99c9ea6a98cbae6713c03f4ac4a6b87cf682a9189c073f29ecd5845d3ddc5c7e
5
5
  SHA512:
6
- metadata.gz: 5fa9ddee2ccb22bdeb051dc11bf09aa158a93ebf3d95156ae43bea1fb933527e9bb31cf60910d063017c229e599aaa20d7452c792bc2b7f0303da8f20c579bbc
7
- data.tar.gz: 8d95e69cfb5e09c3e4103653bd739e96b37430f7e92720c668a8f396458f07fbeec9e01b25a6b4cff8128ad958f64d3adc01f748a3836f2ea768f8cc60d74244
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.7.8)
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.0.9.1)
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.7.7'
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 not validate certs
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
- ```ruby
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.7.8".freeze
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
- if @streaming
134
- # streaming response (the actual network communication is deferred, a.k.a. streamed)
135
- target_response = HttpStreamingResponse.new(target_request, backend.host, backend.port)
136
- target_response.use_ssl = use_ssl
137
- target_response.read_timeout = read_timeout
138
- target_response.ssl_version = @ssl_version if @ssl_version
139
- target_response.verify_mode = (@verify_mode || OpenSSL::SSL::VERIFY_NONE) if use_ssl
140
- target_response.cert = @cert if @cert
141
- target_response.key = @key if @key
142
- else
143
- http = Net::HTTP.new(backend.host, backend.port)
144
- http.use_ssl = use_ssl if use_ssl
145
- http.read_timeout = read_timeout
146
- http.ssl_version = @ssl_version if @ssl_version
147
- http.verify_mode = (@verify_mode || OpenSSL::SSL::VERIFY_NONE if use_ssl) if use_ssl
148
- http.cert = @cert if @cert
149
- http.key = @key if @key
150
-
151
- target_response = http.start do
152
- http.request(target_request)
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
- code = target_response.code
157
- headers = self.class.normalize_headers(target_response.respond_to?(:headers) ? target_response.headers : target_response.to_hash)
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
- Rails.application.config.middleware.use TrustingProxy, backend: 'https://self-signed.badssl.com', streaming: false
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
@@ -22,4 +22,5 @@ Gem::Specification.new do |s|
22
22
  s.add_dependency("rack")
23
23
  s.add_development_dependency("rack-test")
24
24
  s.add_development_dependency("test-unit")
25
+ s.add_development_dependency("webrick")
25
26
  end
@@ -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.7.8
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: