rack-proxy 0.8.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,358 +0,0 @@
1
- require "test_helper"
2
- require "rack/proxy"
3
-
4
- class RackProxyTest < Test::Unit::TestCase
5
- class HostProxy < Rack::Proxy
6
- attr_accessor :host
7
-
8
- def rewrite_env(env)
9
- env["HTTP_HOST"] = self.host || 'example.com'
10
- env
11
- end
12
- end
13
-
14
- def app(opts = {})
15
- return @app ||= HostProxy.new(opts)
16
- end
17
-
18
- def test_http_streaming
19
- get "/"
20
- assert last_response.ok?
21
-
22
- assert_match(/Example Domain/, last_response.body)
23
- end
24
-
25
- def test_http_full_request
26
- app(:streaming => false)
27
- get "/"
28
- assert last_response.ok?
29
- assert_match(/Example Domain/, last_response.body)
30
- end
31
-
32
- def test_http_full_request_headers
33
- app(:streaming => false)
34
- app.host = 'httpbin.org'
35
- get "/cookies/set?test=1"
36
- assert !Array(last_response['Set-Cookie']).empty?, 'httpbin.org/cookies/set should set a cookie'
37
- end
38
-
39
- def test_https_streaming
40
- app.host = 'www.apple.com'
41
- get 'https://example.com'
42
- assert last_response.ok?
43
- assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body)
44
- end
45
-
46
- def test_https_streaming_tls
47
- app(:ssl_version => :TLSv1_2).host = 'www.apple.com'
48
- get 'https://example.com'
49
- assert last_response.ok?
50
- assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body)
51
- end
52
-
53
- def test_https_full_request
54
- app(:streaming => false).host = 'www.apple.com'
55
- get 'https://example.com'
56
- assert last_response.ok?
57
- assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body)
58
- end
59
-
60
- def test_https_full_request_tls
61
- app({:streaming => false, :ssl_version => :TLSv1_2}).host = 'www.apple.com'
62
- get 'https://example.com'
63
- assert last_response.ok?
64
- assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body)
65
- end
66
-
67
- def test_normalize_headers
68
- proxy_class = Rack::Proxy
69
- headers = { 'header_array' => ['first_entry'], 'header_non_array' => :entry }
70
-
71
- normalized_headers = proxy_class.send(:normalize_headers, headers)
72
- expected_class = Rack.const_defined?(:Headers) ? Rack::Headers : Rack::Utils::HeaderHash
73
- assert normalized_headers.instance_of?(expected_class)
74
- assert normalized_headers['header_array'] == 'first_entry'
75
- assert normalized_headers['header_non_array'] == :entry
76
- end
77
-
78
- def test_header_reconstruction
79
- proxy_class = Rack::Proxy
80
-
81
- header = proxy_class.send(:reconstruct_header_name, "HTTP_ABC")
82
- assert header == "Abc"
83
-
84
- header = proxy_class.send(:reconstruct_header_name, "HTTP_ABC_D")
85
- assert header == "Abc-D"
86
- end
87
-
88
- def test_extract_http_request_headers
89
- proxy_class = Rack::Proxy
90
- env = {
91
- 'NOT-HTTP-HEADER' => 'test-value',
92
- 'HTTP_ACCEPT' => 'text/html',
93
- 'HTTP_CONNECTION' => nil,
94
- 'HTTP_CONTENT_MD5' => 'deadbeef',
95
- 'HTTP_HEADER.WITH.PERIODS' => 'stillmooing'
96
- }
97
-
98
- headers = proxy_class.extract_http_request_headers(env)
99
- assert headers.key?('ACCEPT')
100
- assert headers.key?('CONTENT-MD5')
101
- assert headers.key?('HEADER.WITH.PERIODS')
102
- assert !headers.key?('CONNECTION')
103
- assert !headers.key?('NOT-HTTP-HEADER')
104
- end
105
-
106
- def test_duplicate_headers
107
- proxy_class = Rack::Proxy
108
- env = { 'Set-Cookie' => ["cookie1=foo", "cookie2=bar"] }
109
-
110
- headers = proxy_class.normalize_headers(env)
111
- assert headers['Set-Cookie'].include?('cookie1=foo'), "Include the first value"
112
- assert headers['Set-Cookie'].include?("\n"), "Join multiple cookies with newlines"
113
- assert headers['Set-Cookie'].include?('cookie2=bar'), "Include the second value"
114
- end
115
-
116
-
117
- def test_handles_missing_content_length
118
- assert_nothing_thrown do
119
- post "/", nil, "CONTENT_LENGTH" => nil
120
- end
121
- end
122
-
123
- # An input stream that deliberately omits #rewind, mimicking
124
- # Rackup::Handler::WEBrick::Input under Rack 3 (the Rack 3 SPEC no longer
125
- # requires the input stream to respond to #rewind, only gets/each/read).
126
- class NonRewindableInput
127
- def initialize(string)
128
- @io = StringIO.new(string)
129
- end
130
-
131
- def read(*args) = @io.read(*args)
132
- def gets(*args) = @io.gets(*args)
133
- def each(&block) = @io.each(&block)
134
- # intentionally NO #rewind
135
- end
136
-
137
- # Issue #128: a non-rewindable body stream must not raise (it used to blow up
138
- # with NoMethodError on #rewind, surfacing confusingly as a 500). The body
139
- # must still be forwarded intact, since it is never read before Net::HTTP sends it.
140
- def test_non_rewindable_body_is_forwarded_without_raising
141
- with_webrick_proxy(streaming: false) do |port, proxy|
142
- proxy.host = "127.0.0.1:#{port}"
143
-
144
- body = NonRewindableInput.new("hello=world")
145
- assert !body.respond_to?(:rewind), "fixture must not be rewindable to exercise the guard"
146
-
147
- env = Rack::MockRequest.env_for("/echo-body", method: "POST")
148
- env["rack.input"] = body
149
- env["CONTENT_LENGTH"] = "hello=world".bytesize.to_s
150
- env["CONTENT_TYPE"] = "text/plain"
151
-
152
- status, _headers, response = nil
153
- assert_nothing_raised { status, _headers, response = proxy.call(env) }
154
- assert_equal 200, status.to_i
155
- assert_equal "hello=world", response.to_a.join, "body must be forwarded intact"
156
- end
157
- end
158
-
159
- def test_response_header_included_Hop_by_hop
160
- app({:streaming => true}).host = 'mockapi.io'
161
- get 'https://example.com/oauth2/token/info?access_token=123'
162
- assert !last_response.headers.key?('transfer-encoding')
163
- end
164
-
165
- # Issue #58: connection errors should return 502, not raise.
166
- def test_connection_refused_returns_502
167
- # Bind a socket to find a free port, then close it so connection is refused.
168
- server = TCPServer.new('127.0.0.1', 0)
169
- closed_port = server.addr[1]
170
- server.close
171
-
172
- app({:streaming => false}).host = "127.0.0.1:#{closed_port}"
173
- get '/'
174
- assert_equal 502, last_response.status
175
- assert_equal '', last_response.body
176
- end
177
-
178
- def test_connection_refused_returns_502_streaming
179
- server = TCPServer.new('127.0.0.1', 0)
180
- closed_port = server.addr[1]
181
- server.close
182
-
183
- app({:streaming => true}).host = "127.0.0.1:#{closed_port}"
184
- get '/'
185
- assert_equal 502, last_response.status
186
- assert_equal '', last_response.body
187
- end
188
-
189
- def test_unknown_host_returns_502
190
- app({:streaming => false}).host = 'no-such-host.invalid'
191
- get '/'
192
- assert_equal 502, last_response.status
193
- end
194
-
195
- # Issues #122/#123: body should be [] for empty responses and for status
196
- # codes that don't allow an entity body (1xx, 204, 304).
197
- def test_no_entity_body_for_204
198
- with_webrick_proxy(streaming: false) do |port, proxy|
199
- proxy.host = "127.0.0.1:#{port}"
200
- get '/no-content'
201
- assert_equal 204, last_response.status
202
- assert_equal '', last_response.body
203
- end
204
- end
205
-
206
- def test_no_entity_body_for_304
207
- with_webrick_proxy(streaming: false) do |port, proxy|
208
- proxy.host = "127.0.0.1:#{port}"
209
- get '/not-modified'
210
- assert_equal 304, last_response.status
211
- assert_equal '', last_response.body
212
- end
213
- end
214
-
215
- def test_empty_body_is_not_array_with_empty_string
216
- with_webrick_proxy(streaming: false) do |port, proxy|
217
- proxy.host = "127.0.0.1:#{port}"
218
- get '/empty'
219
- assert_equal 200, last_response.status
220
- assert_equal '', last_response.body
221
- end
222
- end
223
-
224
- # Issue #65: header values must be strings, not single-element arrays,
225
- # for both streaming and non-streaming paths.
226
- def test_header_values_are_strings_streaming
227
- assert_no_array_header_values(streaming: true)
228
- end
229
-
230
- def test_header_values_are_strings_non_streaming
231
- assert_no_array_header_values(streaming: false)
232
- end
233
-
234
- # Issue #113: SSL cert verification must default to VERIFY_PEER (Ruby's
235
- # Net::HTTP default), not VERIFY_NONE.
236
- def test_ssl_default_is_verify_peer
237
- proxy = Rack::Proxy.new
238
- assert_nil proxy.instance_variable_get(:@verify_mode),
239
- "@verify_mode should be unset by default so VERIFY_PEER applies at request time"
240
- end
241
-
242
- def test_ssl_verify_none_opt_in
243
- proxy = Rack::Proxy.new(ssl_verify_none: true)
244
- assert_equal OpenSSL::SSL::VERIFY_NONE, proxy.instance_variable_get(:@verify_mode)
245
- end
246
-
247
- def test_explicit_verify_mode_wins_over_ssl_verify_none
248
- proxy = Rack::Proxy.new(ssl_verify_none: true, verify_mode: OpenSSL::SSL::VERIFY_PEER)
249
- assert_equal OpenSSL::SSL::VERIFY_PEER, proxy.instance_variable_get(:@verify_mode)
250
- end
251
-
252
- def test_https_default_rejects_invalid_certificate
253
- # self-signed cert on a public test host should be rejected with the new default
254
- app({:streaming => false}).host = 'self-signed.badssl.com'
255
- error = assert_raise(OpenSSL::SSL::SSLError) { get 'https://example.com/' }
256
- assert_match(/certificate verify failed/, error.message)
257
- end
258
-
259
- def test_https_with_ssl_verify_none_accepts_invalid_certificate
260
- app({:streaming => false, :ssl_verify_none => true}).host = 'self-signed.badssl.com'
261
- get 'https://example.com/'
262
- assert last_response.ok?
263
- end
264
-
265
- # Issue #80: a :logger option should pipe Net::HTTP debug output to the
266
- # given sink (anything responding to #<<). We use a StringIO to capture it.
267
- def test_logger_captures_request_in_non_streaming
268
- sink = StringIO.new
269
- with_webrick_proxy(streaming: false, logger: sink) do |port, proxy|
270
- proxy.host = "127.0.0.1:#{port}"
271
- get '/empty'
272
- assert last_response.ok?
273
- end
274
- assert_match(/GET \/empty/, sink.string,
275
- "expected debug output to include request line, got: #{sink.string.inspect}")
276
- end
277
-
278
- def test_logger_captures_request_in_streaming
279
- sink = StringIO.new
280
- with_webrick_proxy(streaming: true, logger: sink) do |port, proxy|
281
- proxy.host = "127.0.0.1:#{port}"
282
- get '/empty'
283
- assert last_response.ok?
284
- end
285
- assert_match(/GET \/empty/, sink.string,
286
- "expected debug output to include request line, got: #{sink.string.inspect}")
287
- end
288
-
289
- # Regression: build_header_hash must not match a top-level ::Headers
290
- # constant defined by the host app (would happen with inherit: true).
291
- def test_build_header_hash_ignores_toplevel_headers_constant
292
- Object.send(:remove_const, :Headers) if Object.const_defined?(:Headers, false)
293
- Object.const_set(:Headers, Class.new)
294
- begin
295
- result = Rack::Proxy.send(:build_header_hash, [['X-Test', 'value']])
296
- # On Rack 3+ we get Rack::Headers; on Rack 2 we get Rack::Utils::HeaderHash.
297
- # In neither case should we get the bogus top-level ::Headers.
298
- assert_not_equal ::Headers, result.class,
299
- "build_header_hash leaked into top-level ::Headers"
300
- ensure
301
- Object.send(:remove_const, :Headers)
302
- end
303
- end
304
-
305
- def test_no_logger_means_no_debug_output
306
- # Without a :logger option, Net::HTTP's set_debug_output should never be
307
- # called. We can't directly assert that, but we can confirm requests still
308
- # work when no logger is configured (covered by every other test).
309
- with_webrick_proxy(streaming: false) do |port, proxy|
310
- proxy.host = "127.0.0.1:#{port}"
311
- get '/empty'
312
- assert last_response.ok?
313
- end
314
- end
315
-
316
- private
317
-
318
- def assert_no_array_header_values(streaming:)
319
- with_webrick_proxy(streaming: streaming) do |port, proxy|
320
- proxy.host = "127.0.0.1:#{port}"
321
- get '/echo-headers'
322
- array_valued = last_response.headers.select { |_, v| v.is_a?(Array) }
323
- assert_empty array_valued,
324
- "expected no Array-valued headers (#65), got: #{array_valued.inspect}"
325
- assert_equal 'value-here', last_response['x-custom']
326
- end
327
- end
328
-
329
-
330
- # Spin up a tiny WEBrick server with fixed routes so we can exercise the
331
- # proxy against real Net::HTTP requests without depending on a remote host.
332
- def with_webrick_proxy(**proxy_opts)
333
- require 'webrick'
334
- server = WEBrick::HTTPServer.new(
335
- Port: 0,
336
- BindAddress: '127.0.0.1',
337
- Logger: WEBrick::Log.new(File::NULL),
338
- AccessLog: []
339
- )
340
- server.mount_proc('/no-content') { |_req, res| res.status = 204 }
341
- server.mount_proc('/not-modified') { |_req, res| res.status = 304 }
342
- server.mount_proc('/empty') { |_req, res| res.body = '' }
343
- server.mount_proc('/echo-headers') do |_req, res|
344
- res['x-custom'] = 'value-here'
345
- res.body = 'ok'
346
- end
347
- server.mount_proc('/echo-body') { |req, res| res.body = req.body.to_s }
348
- Thread.new { server.start }
349
- port = server.config[:Port]
350
-
351
- proxy = HostProxy.new(**proxy_opts)
352
- @app = proxy
353
- yield port, proxy
354
- ensure
355
- server&.shutdown
356
- @app = nil
357
- end
358
- end
data/test/test_helper.rb DELETED
@@ -1,11 +0,0 @@
1
- require "rubygems"
2
- require 'bundler/setup'
3
- require 'bundler/gem_tasks'
4
- require "test/unit"
5
-
6
- require "rack"
7
- require "rack/test"
8
-
9
- Test::Unit::TestCase.class_eval do
10
- include Rack::Test::Methods
11
- end