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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +184 -0
- data/LICENSE +1 -1
- data/README.md +254 -166
- 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
data/test/rack_proxy_test.rb
DELETED
|
@@ -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
|