rack-reverse-proxy-pact 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 +7 -0
- data/.document +5 -0
- data/.github/workflows/test.yml +22 -0
- data/.gitignore +16 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +41 -0
- data/Gemfile +20 -0
- data/LICENSE +20 -0
- data/README.md +129 -0
- data/Rakefile +10 -0
- data/lib/rack/reverse_proxy.rb +6 -0
- data/lib/rack_reverse_proxy/errors.rb +36 -0
- data/lib/rack_reverse_proxy/middleware.rb +47 -0
- data/lib/rack_reverse_proxy/response_builder.rb +60 -0
- data/lib/rack_reverse_proxy/roundtrip.rb +286 -0
- data/lib/rack_reverse_proxy/rule.rb +206 -0
- data/lib/rack_reverse_proxy/version.rb +4 -0
- data/lib/rack_reverse_proxy.rb +8 -0
- data/rack-reverse-proxy.gemspec +49 -0
- data/script/rubocop +5 -0
- data/spec/rack/reverse_proxy_spec.rb +768 -0
- data/spec/rack_reverse_proxy/response_builder_spec.rb +37 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/support/http_streaming_response_patch.rb +32 -0
- metadata +168 -0
@@ -0,0 +1,768 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "cgi"
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
RSpec.describe Rack::ReverseProxy do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
def app
|
9
|
+
Rack::ReverseProxy.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def dummy_app
|
13
|
+
lambda { |_| [200, {}, ["Dummy App"]] }
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:http_streaming_response) do
|
17
|
+
double(
|
18
|
+
"Rack::HttpStreamingResponse",
|
19
|
+
:use_ssl= => nil,
|
20
|
+
:verify_mode= => nil,
|
21
|
+
:headers => {},
|
22
|
+
:status => 200,
|
23
|
+
:body => "OK"
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "global options" do
|
28
|
+
it "starts with default global options" do
|
29
|
+
m = Rack::ReverseProxy.new(dummy_app) do
|
30
|
+
reverse_proxy "/test", "http://example.com/"
|
31
|
+
end
|
32
|
+
expect(m.instance_variable_get(:@global_options)).to eq(RackReverseProxy::Middleware::DEFAULT_OPTIONS)
|
33
|
+
end
|
34
|
+
it "allows options to be set via reverse_proxy_options, maintains global defaults" do
|
35
|
+
m = Rack::ReverseProxy.new(dummy_app) do
|
36
|
+
reverse_proxy "/test", "http://example.com/"
|
37
|
+
reverse_proxy_options preserve_host: "preserve_host_val"
|
38
|
+
end
|
39
|
+
expect(m.instance_variable_get(:@global_options)).to_not eq(RackReverseProxy::Middleware::DEFAULT_OPTIONS)
|
40
|
+
expect(m.instance_variable_get(:@global_options)[:preserve_host]).to eq("preserve_host_val")
|
41
|
+
raise "necessary condition for test is missing" if RackReverseProxy::Middleware::DEFAULT_OPTIONS[:x_forwarded_headers].nil?
|
42
|
+
expect(m.instance_variable_get(:@global_options)[:x_forwarded_headers]).to eq(RackReverseProxy::Middleware::DEFAULT_OPTIONS[:x_forwarded_headers])
|
43
|
+
end
|
44
|
+
it "supports multiple commulative invocations of reverse_proxy_options" do
|
45
|
+
m = Rack::ReverseProxy.new(dummy_app) do
|
46
|
+
reverse_proxy "/test", "http://example.com/"
|
47
|
+
reverse_proxy_options preserve_host: "preserve_host_val", stripped_headers: ["foo"]
|
48
|
+
reverse_proxy_options replace_response_host: "replace_response_host_val", stripped_headers: ["bar"]
|
49
|
+
end
|
50
|
+
expect(m.instance_variable_get(:@global_options)[:preserve_host]).to eq("preserve_host_val")
|
51
|
+
expect(m.instance_variable_get(:@global_options)[:replace_response_host]).to eq("replace_response_host_val")
|
52
|
+
expect(m.instance_variable_get(:@global_options)[:stripped_headers]).to eq(["bar"])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "as middleware" do
|
57
|
+
def app
|
58
|
+
Rack::ReverseProxy.new(dummy_app) do
|
59
|
+
reverse_proxy "/test", "http://example.com/", :preserve_host => true
|
60
|
+
reverse_proxy "/2test", lambda { |_| "http://example.com/" }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "forwards requests to the calling app when the path is not matched" do
|
65
|
+
get "/"
|
66
|
+
expect(last_response.body).to eq("Dummy App")
|
67
|
+
expect(last_response).to be_ok
|
68
|
+
end
|
69
|
+
|
70
|
+
it "proxies requests when a pattern is matched" do
|
71
|
+
stub_request(:get, "http://example.com/test").to_return(:body => "Proxied App")
|
72
|
+
get "/test"
|
73
|
+
expect(last_response.body).to eq("Proxied App")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "produces a response header of type Headers" do
|
77
|
+
stub_request(:get, "http://example.com/test")
|
78
|
+
get "/test"
|
79
|
+
expected_class = rack_version_less_than_three ? Rack::Utils::HeaderHash : Rack::Headers
|
80
|
+
expect(last_response.headers).to be_an_instance_of(expected_class)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "parses the headers as a Hash with values of type String" do
|
84
|
+
stub_request(:get, "http://example.com/test").to_return(
|
85
|
+
:headers => { "cache-control" => "max-age=300, public" }
|
86
|
+
)
|
87
|
+
get "/test"
|
88
|
+
expect(last_response.headers["cache-control"]).to be_an_instance_of(String)
|
89
|
+
expect(last_response.headers["cache-control"]).to eq("max-age=300, public")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "proxies requests to a lambda url when a pattern is matched" do
|
93
|
+
stub_request(:get, "http://example.com/2test").to_return(:body => "Proxied App2")
|
94
|
+
get "/2test"
|
95
|
+
expect(last_response.body).to eq("Proxied App2")
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns headers from proxied app as strings" do
|
99
|
+
stub_request(:get, "http://example.com/test").to_return(
|
100
|
+
:body => "Proxied App",
|
101
|
+
:headers => { "Proxied-Header" => "TestValue" }
|
102
|
+
)
|
103
|
+
get "/test"
|
104
|
+
expect(last_response.headers["Proxied-Header"]).to eq("TestValue")
|
105
|
+
end
|
106
|
+
|
107
|
+
it "sets the Host header w/o default port" do
|
108
|
+
stub_request(:any, "example.com/test/stuff")
|
109
|
+
get "/test/stuff"
|
110
|
+
expect(
|
111
|
+
a_request(:get, "http://example.com/test/stuff").with(
|
112
|
+
:headers => { "Host" => "example.com" }
|
113
|
+
)
|
114
|
+
).to have_been_made
|
115
|
+
end
|
116
|
+
|
117
|
+
it "sets the X-Forwarded-Host header to the proxying host by default" do
|
118
|
+
stub_request(:any, "example.com/test/stuff")
|
119
|
+
get "/test/stuff"
|
120
|
+
expect(
|
121
|
+
a_request(:get, "http://example.com/test/stuff").with(
|
122
|
+
:headers => { "X-Forwarded-Host" => "example.org" }
|
123
|
+
)
|
124
|
+
).to have_been_made
|
125
|
+
end
|
126
|
+
|
127
|
+
it "sets the X-Forwarded-Port header to the proxying port by default" do
|
128
|
+
stub_request(:any, "example.com/test/stuff")
|
129
|
+
get "/test/stuff"
|
130
|
+
expect(
|
131
|
+
a_request(:get, "http://example.com/test/stuff").with(
|
132
|
+
:headers => { "X-Forwarded-Port" => "80" }
|
133
|
+
)
|
134
|
+
).to have_been_made
|
135
|
+
end
|
136
|
+
|
137
|
+
it "sets the X-Forwarded-Proto header to the proxying scheme by default" do
|
138
|
+
stub_request(:any, "example.com/test/stuff")
|
139
|
+
get "https://example.com/test/stuff"
|
140
|
+
expect(
|
141
|
+
a_request(:get, "example.com/test/stuff").with(
|
142
|
+
:headers => { "X-Forwarded-Proto" => "https" }
|
143
|
+
)
|
144
|
+
).to have_been_made
|
145
|
+
end
|
146
|
+
|
147
|
+
it "does not produce headers with a Status key" do
|
148
|
+
stub_request(:get, "http://example.com/2test").to_return(
|
149
|
+
:status => 301, :headers => { :status => "301 Moved Permanently" }
|
150
|
+
)
|
151
|
+
|
152
|
+
get "/2test"
|
153
|
+
|
154
|
+
headers = last_response.headers.to_hash
|
155
|
+
expect(headers["Status"]).to be_nil
|
156
|
+
end
|
157
|
+
|
158
|
+
it "compares keys case-insensitive" do
|
159
|
+
stub_request(:get, "http://example.com/2test").to_return(
|
160
|
+
:headers => { :date => "Wed, 22 Jul 2015 11:27:21 GMT" }
|
161
|
+
)
|
162
|
+
|
163
|
+
get "/2test"
|
164
|
+
|
165
|
+
headers = last_response.headers.to_hash
|
166
|
+
expect(headers["Date"]).to eq("Wed, 22 Jul 2015 11:27:21 GMT")
|
167
|
+
expect(headers["date"]).to rack_version_less_than_three ? be_nil : eq("Wed, 22 Jul 2015 11:27:21 GMT")
|
168
|
+
end
|
169
|
+
|
170
|
+
it "formats the headers with dashes correctly" do
|
171
|
+
stub_request(:get, "http://example.com/2test").to_return(
|
172
|
+
:status => 301,
|
173
|
+
:headers => { :status => "301 Moved Permanently", :"x-additional-info" => "something" }
|
174
|
+
)
|
175
|
+
|
176
|
+
get "/2test"
|
177
|
+
|
178
|
+
headers = last_response.headers.to_hash
|
179
|
+
expect(headers["x-additional-info"]).to eq(rack_version_less_than_three ? nil : "something")
|
180
|
+
end
|
181
|
+
|
182
|
+
it "the response header includes content-length" do
|
183
|
+
body = "this is the test body"
|
184
|
+
stub_request(:any, "example.com/test/stuff").to_return(
|
185
|
+
:body => body, :headers => { "Content-Length" => "10" }
|
186
|
+
)
|
187
|
+
get "/test/stuff"
|
188
|
+
expect(last_response.headers["Content-Length"]).to eq(body.length.to_s)
|
189
|
+
end
|
190
|
+
|
191
|
+
describe "with non-default port" do
|
192
|
+
def app
|
193
|
+
Rack::ReverseProxy.new(dummy_app) do
|
194
|
+
reverse_proxy "/test", "http://example.com:8080/"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it "sets the Host header including non-default port" do
|
199
|
+
stub_request(:any, "example.com:8080/test/stuff")
|
200
|
+
get "/test/stuff"
|
201
|
+
expect(
|
202
|
+
a_request(:get, "http://example.com:8080/test/stuff").with(
|
203
|
+
:headers => { "Host" => "example.com:8080" }
|
204
|
+
)
|
205
|
+
).to have_been_made
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe "with preserve host turned off" do
|
210
|
+
def app
|
211
|
+
Rack::ReverseProxy.new(dummy_app) do
|
212
|
+
reverse_proxy "/test", "http://example.com/", :preserve_host => false
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
it "does not set the Host header" do
|
217
|
+
stub_request(:any, "example.com/test/stuff")
|
218
|
+
get "/test/stuff"
|
219
|
+
|
220
|
+
expect(
|
221
|
+
a_request(:get, "http://example.com/test/stuff").with(
|
222
|
+
:headers => { "Host" => "example.com" }
|
223
|
+
)
|
224
|
+
).not_to have_been_made
|
225
|
+
|
226
|
+
expect(a_request(:get, "http://example.com/test/stuff")).to have_been_made
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context "stripped_headers option" do
|
231
|
+
subject do
|
232
|
+
stub_request(:any, "http://example.com/test")
|
233
|
+
get "/test", {}, "HTTP_ACCEPT_ENCODING" => "gzip, deflate", "HTTP_FOO_BAR" => "baz"
|
234
|
+
end
|
235
|
+
|
236
|
+
describe "with stripped_headers not set" do
|
237
|
+
def app
|
238
|
+
Rack::ReverseProxy.new(dummy_app) do
|
239
|
+
reverse_proxy "/test", "http://example.com/"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
it "forwards the headers" do
|
244
|
+
subject
|
245
|
+
expect(
|
246
|
+
a_request(:get, "http://example.com/test").with(
|
247
|
+
:headers => { "Accept-Encoding" => "gzip, deflate", "Foo-Bar" => "baz" }
|
248
|
+
)
|
249
|
+
).to have_been_made
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
describe "with stripped_headers set" do
|
254
|
+
before do
|
255
|
+
@stripped_headers = ["Accept-Encoding", "Foo-Bar"]
|
256
|
+
def app
|
257
|
+
# so the value is constant in the closure below
|
258
|
+
stripped_headers = @stripped_headers
|
259
|
+
Rack::ReverseProxy.new(dummy_app) do
|
260
|
+
reverse_proxy "/test", "http://example.com/", :stripped_headers => stripped_headers
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it "removes the stripped headers" do
|
266
|
+
subject
|
267
|
+
expect(
|
268
|
+
a_request(:get, "http://example.com/test").with{ |req|
|
269
|
+
req.headers.each do |header, value|
|
270
|
+
if @stripped_headers.include?(header)
|
271
|
+
fail "expected #{header} to not be present"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
}
|
275
|
+
).to have_been_made
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
describe "with x_forwarded_headers turned off" do
|
281
|
+
def app
|
282
|
+
Rack::ReverseProxy.new(dummy_app) do
|
283
|
+
reverse_proxy_options :x_forwarded_headers => false
|
284
|
+
reverse_proxy "/test", "http://example.com/"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
it "does not set the X-Forwarded-Host header to the proxying host" do
|
289
|
+
stub_request(:any, "example.com/test/stuff")
|
290
|
+
get "/test/stuff"
|
291
|
+
expect(
|
292
|
+
a_request(:get, "http://example.com/test/stuff").with(
|
293
|
+
:headers => { "X-Forwarded-Host" => "example.org" }
|
294
|
+
)
|
295
|
+
).not_to have_been_made
|
296
|
+
expect(a_request(:get, "http://example.com/test/stuff")).to have_been_made
|
297
|
+
end
|
298
|
+
|
299
|
+
it "does not set the X-Forwarded-Port header to the proxying port" do
|
300
|
+
stub_request(:any, "example.com/test/stuff")
|
301
|
+
get "/test/stuff"
|
302
|
+
expect(
|
303
|
+
a_request(:get, "http://example.com/test/stuff").with(
|
304
|
+
:headers => { "X-Forwarded-Port" => "80" }
|
305
|
+
)
|
306
|
+
).not_to have_been_made
|
307
|
+
expect(a_request(:get, "http://example.com/test/stuff")).to have_been_made
|
308
|
+
end
|
309
|
+
|
310
|
+
it "does not set the X-Forwarded-Proto header to the proxying scheme" do
|
311
|
+
stub_request(:any, "example.com/test/stuff")
|
312
|
+
get "https://example.com/test/stuff"
|
313
|
+
expect(
|
314
|
+
a_request(:get, "example.com/test/stuff").with(
|
315
|
+
:headers => { "X-Forwarded-Proto" => "https" }
|
316
|
+
)
|
317
|
+
).not_to have_been_made
|
318
|
+
expect(a_request(:get, "example.com/test/stuff")).to have_been_made
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe "with timeout configuration" do
|
323
|
+
def app
|
324
|
+
Rack::ReverseProxy.new(dummy_app) do
|
325
|
+
reverse_proxy "/test/slow", "http://example.com/", :timeout => 99
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
it "makes request with basic auth" do
|
330
|
+
stub_request(:get, "http://example.com/test/slow")
|
331
|
+
allow(Rack::HttpStreamingResponse).to receive(:new).and_return(http_streaming_response)
|
332
|
+
expect(http_streaming_response).to receive(:read_timeout=).with(99)
|
333
|
+
get "/test/slow"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
describe "without timeout configuration" do
|
338
|
+
def app
|
339
|
+
Rack::ReverseProxy.new(dummy_app) do
|
340
|
+
reverse_proxy "/test/slow", "http://example.com/"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
it "makes request with basic auth" do
|
345
|
+
stub_request(:get, "http://example.com/test/slow")
|
346
|
+
allow(Rack::HttpStreamingResponse).to receive(:new).and_return(http_streaming_response)
|
347
|
+
expect(http_streaming_response).not_to receive(:read_timeout=)
|
348
|
+
get "/test/slow"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
describe "with basic auth turned on" do
|
353
|
+
def app
|
354
|
+
Rack::ReverseProxy.new(dummy_app) do
|
355
|
+
reverse_proxy "/test", "http://example.com/", :username => "joe", :password => "shmoe"
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
it "makes request with basic auth" do
|
360
|
+
stub_request(:get, "http://example.com/test/stuff").with(
|
361
|
+
:basic_auth => %w(joe shmoe)
|
362
|
+
).to_return(
|
363
|
+
:body => "secured content"
|
364
|
+
)
|
365
|
+
get "/test/stuff"
|
366
|
+
expect(last_response.body).to eq("secured content")
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
describe "with replace response host turned on" do
|
371
|
+
def app
|
372
|
+
Rack::ReverseProxy.new(dummy_app) do
|
373
|
+
reverse_proxy "/test", "http://example.com/", :replace_response_host => true
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
it "replaces the location response header" do
|
378
|
+
stub_request(:get, "http://example.com/test/stuff").to_return(
|
379
|
+
:headers => { "location" => "http://test.com/bar" }
|
380
|
+
)
|
381
|
+
get "http://example.com/test/stuff"
|
382
|
+
expect(last_response.headers["location"]).to eq("http://example.com/bar")
|
383
|
+
end
|
384
|
+
|
385
|
+
it "keeps the port of the location" do
|
386
|
+
stub_request(:get, "http://example.com/test/stuff").to_return(
|
387
|
+
:headers => { "location" => "http://test.com/bar" }
|
388
|
+
)
|
389
|
+
get "http://example.com:3000/test/stuff"
|
390
|
+
expect(last_response.headers["location"]).to eq("http://example.com:3000/bar")
|
391
|
+
end
|
392
|
+
|
393
|
+
it "doesn't keep the port when it's default for the protocol" do
|
394
|
+
# webmock doesn't allow to stub an https URI, but this is enough to
|
395
|
+
# reply to the https code path
|
396
|
+
stub_request(:get, "http://example.com/test/stuff").to_return(
|
397
|
+
:headers => { "location" => "http://test.com/bar" }
|
398
|
+
)
|
399
|
+
get "https://example.com/test/stuff"
|
400
|
+
expect(last_response.headers["location"]).to eq("https://example.com/bar")
|
401
|
+
end
|
402
|
+
|
403
|
+
it "doesn't replaces the location response header if it has no host" do
|
404
|
+
stub_request(:get, "http://example.com/test/stuff").to_return(
|
405
|
+
:headers => { "location" => "/bar" }
|
406
|
+
)
|
407
|
+
get "http://example.com/test/stuff"
|
408
|
+
expect(last_response.headers["location"]).to eq("/bar")
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
describe "with ambiguous routes and all matching" do
|
413
|
+
def app
|
414
|
+
Rack::ReverseProxy.new(dummy_app) do
|
415
|
+
reverse_proxy_options :matching => :all
|
416
|
+
reverse_proxy "/test", "http://example.com/"
|
417
|
+
reverse_proxy(%r{^/test}, "http://example.com/")
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
it "raises an exception" do
|
422
|
+
expect { get "/test" }.to raise_error(RackReverseProxy::Errors::AmbiguousMatch)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# FIXME: descriptions are not consistent with examples
|
427
|
+
describe "with ambiguous routes and first matching" do
|
428
|
+
def app
|
429
|
+
Rack::ReverseProxy.new(dummy_app) do
|
430
|
+
reverse_proxy_options :matching => :first
|
431
|
+
reverse_proxy "/test", "http://example1.com/"
|
432
|
+
reverse_proxy(%r{^/test}, "http://example2.com/")
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
it "raises an exception" do
|
437
|
+
stub_request(:get, "http://example1.com/test").to_return(:body => "Proxied App")
|
438
|
+
get "/test"
|
439
|
+
expect(last_response.body).to eq("Proxied App")
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
describe "with force ssl turned on" do
|
444
|
+
def app
|
445
|
+
Rack::ReverseProxy.new(dummy_app) do
|
446
|
+
reverse_proxy "/test", "http://example1.com/",
|
447
|
+
:force_ssl => true, :replace_response_host => true
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
it "redirects to the ssl version when requesting non-ssl" do
|
452
|
+
stub_request(:get, "http://example1.com/test/stuff").to_return(:body => "proxied")
|
453
|
+
get "http://example.com/test/stuff"
|
454
|
+
expect(last_response.headers["Location"]).to eq("https://example.com/test/stuff")
|
455
|
+
end
|
456
|
+
|
457
|
+
it "does nothing when already ssl" do
|
458
|
+
stub_request(:get, "http://example1.com/test/stuff").to_return(:body => "proxied")
|
459
|
+
get "https://example.com/test/stuff"
|
460
|
+
expect(last_response.body).to eq("proxied")
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
describe "with a route as a regular expression" do
|
465
|
+
def app
|
466
|
+
Rack::ReverseProxy.new(dummy_app) do
|
467
|
+
reverse_proxy %r{^/test(/.*)$}, "http://example.com$1"
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
it "supports subcaptures" do
|
472
|
+
stub_request(:get, "http://example.com/path").to_return(:body => "Proxied App")
|
473
|
+
get "/test/path"
|
474
|
+
expect(last_response.body).to eq("Proxied App")
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
describe "with a https route" do
|
479
|
+
def app
|
480
|
+
Rack::ReverseProxy.new(dummy_app) do
|
481
|
+
reverse_proxy "/test", "https://example.com"
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
it "makes a secure request" do
|
486
|
+
stub_request(:get, "https://example.com/test/stuff").to_return(
|
487
|
+
:body => "Proxied Secure App"
|
488
|
+
)
|
489
|
+
get "/test/stuff"
|
490
|
+
expect(last_response.body).to eq("Proxied Secure App")
|
491
|
+
end
|
492
|
+
|
493
|
+
it "sets the Host header w/o default port" do
|
494
|
+
stub_request(:any, "https://example.com/test/stuff")
|
495
|
+
get "/test/stuff"
|
496
|
+
expect(
|
497
|
+
a_request(:get, "https://example.com/test/stuff").with(
|
498
|
+
:headers => { "Host" => "example.com" }
|
499
|
+
)
|
500
|
+
).to have_been_made
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
describe "with a https route on non-default port" do
|
505
|
+
def app
|
506
|
+
Rack::ReverseProxy.new(dummy_app) do
|
507
|
+
reverse_proxy "/test", "https://example.com:8443"
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
it "sets the Host header including non-default port" do
|
512
|
+
stub_request(:any, "https://example.com:8443/test/stuff")
|
513
|
+
get "/test/stuff"
|
514
|
+
expect(
|
515
|
+
a_request(:get, "https://example.com:8443/test/stuff").with(
|
516
|
+
:headers => { "Host" => "example.com:8443" }
|
517
|
+
)
|
518
|
+
).to have_been_made
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
describe "with a route as a string" do
|
523
|
+
def app
|
524
|
+
Rack::ReverseProxy.new(dummy_app) do
|
525
|
+
reverse_proxy "/test", "http://example.com"
|
526
|
+
reverse_proxy "/path", "http://example.com/foo$0"
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
it "appends the full path to the uri" do
|
531
|
+
stub_request(:get, "http://example.com/test/stuff").to_return(:body => "Proxied App")
|
532
|
+
get "/test/stuff"
|
533
|
+
expect(last_response.body).to eq("Proxied App")
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
describe "with a generic url" do
|
538
|
+
def app
|
539
|
+
Rack::ReverseProxy.new(dummy_app) do
|
540
|
+
reverse_proxy "/test", "example.com"
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
it "throws an exception" do
|
545
|
+
expect { app }.to raise_error(RackReverseProxy::Errors::GenericURI)
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
describe "with a matching route" do
|
550
|
+
def app
|
551
|
+
Rack::ReverseProxy.new(dummy_app) do
|
552
|
+
reverse_proxy "/test", "http://example.com/"
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
%w(get head delete put post).each do |method|
|
557
|
+
describe "and using method #{method}" do
|
558
|
+
it "forwards the correct request" do
|
559
|
+
stub_request(method.to_sym, "http://example.com/test").to_return(
|
560
|
+
:body => "Proxied App for #{method}"
|
561
|
+
)
|
562
|
+
send(method, "/test")
|
563
|
+
expect(last_response.body).to eq("Proxied App for #{method}")
|
564
|
+
end
|
565
|
+
|
566
|
+
if %w(put post).include?(method)
|
567
|
+
it "forwards the request payload" do
|
568
|
+
stub_request(
|
569
|
+
method.to_sym,
|
570
|
+
"http://example.com/test"
|
571
|
+
).to_return { |req| { :body => req.body } }
|
572
|
+
send(method, "/test", :test => "test")
|
573
|
+
expect(last_response.body).to eq("test=test")
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
describe "with a matching lambda" do
|
581
|
+
def app
|
582
|
+
Rack::ReverseProxy.new(dummy_app) do
|
583
|
+
reverse_proxy lambda { |path| path.match(%r{^/test}) }, "http://lambda.example.org"
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
it "forwards requests to the calling app when path is not matched" do
|
588
|
+
get "/users"
|
589
|
+
expect(last_response).to be_ok
|
590
|
+
expect(last_response.body).to eq("Dummy App")
|
591
|
+
end
|
592
|
+
|
593
|
+
it "proxies requests when a pattern is matched" do
|
594
|
+
stub_request(:get, "http://lambda.example.org/test").to_return(:body => "Proxied App")
|
595
|
+
|
596
|
+
get "/test"
|
597
|
+
expect(last_response.body).to eq("Proxied App")
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
describe "with a matching class" do
|
602
|
+
#:nodoc:
|
603
|
+
class Matcher
|
604
|
+
def self.match(path)
|
605
|
+
return unless path =~ %r{^/(test|users)}
|
606
|
+
Matcher.new
|
607
|
+
end
|
608
|
+
|
609
|
+
def url(path)
|
610
|
+
return "http://users-example.com" + path if path.include?("user")
|
611
|
+
"http://example.com" + path
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
def app
|
616
|
+
Rack::ReverseProxy.new(dummy_app) do
|
617
|
+
reverse_proxy Matcher
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
it "forwards requests to the calling app when the path is not matched" do
|
622
|
+
get "/"
|
623
|
+
expect(last_response.body).to eq("Dummy App")
|
624
|
+
expect(last_response).to be_ok
|
625
|
+
end
|
626
|
+
|
627
|
+
it "proxies requests when a pattern is matched" do
|
628
|
+
stub_request(:get, "http://example.com/test").to_return(:body => "Proxied App")
|
629
|
+
stub_request(:get, "http://users-example.com/users").to_return(:body => "User App")
|
630
|
+
|
631
|
+
get "/test"
|
632
|
+
expect(last_response.body).to eq("Proxied App")
|
633
|
+
|
634
|
+
get "/users"
|
635
|
+
expect(last_response.body).to eq("User App")
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
describe "with a matching and transforming class" do
|
640
|
+
#:nodoc:
|
641
|
+
class MatcherAndTransformer
|
642
|
+
def self.match(_path)
|
643
|
+
MatcherAndTransformer.new
|
644
|
+
end
|
645
|
+
|
646
|
+
def url(_path)
|
647
|
+
"http://example.org/redirecting"
|
648
|
+
end
|
649
|
+
|
650
|
+
def transform(response, request_uri)
|
651
|
+
status, headers, body = response
|
652
|
+
location = headers["Location"]
|
653
|
+
headers["Location"] = "?url=" + CGI.escape(location) +
|
654
|
+
"&request_uri=" + CGI.escape(request_uri.to_s)
|
655
|
+
[status, headers, body]
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
def app
|
660
|
+
Rack::ReverseProxy.new(dummy_app) do
|
661
|
+
reverse_proxy MatcherAndTransformer
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
it "transforms the proxied response" do
|
666
|
+
stub_request(:get, "http://example.org/redirecting").to_return(
|
667
|
+
:headers => {
|
668
|
+
"Location" => "http://example.org/target"
|
669
|
+
}
|
670
|
+
)
|
671
|
+
|
672
|
+
get "/"
|
673
|
+
expect(last_response.headers["Location"])
|
674
|
+
.to eq("?url=http%3A%2F%2Fexample.org%2Ftarget" \
|
675
|
+
"&request_uri=http%3A%2F%2Fexample.org%2Fredirecting")
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
describe "with a matching class" do
|
680
|
+
#:nodoc:
|
681
|
+
class RequestMatcher
|
682
|
+
attr_accessor :rackreq
|
683
|
+
|
684
|
+
def initialize(rackreq)
|
685
|
+
self.rackreq = rackreq
|
686
|
+
end
|
687
|
+
|
688
|
+
def self.match(path, _headers, rackreq)
|
689
|
+
return nil unless path =~ %r{^/(test|users)}
|
690
|
+
RequestMatcher.new(rackreq)
|
691
|
+
end
|
692
|
+
|
693
|
+
def url(path)
|
694
|
+
return nil unless rackreq.params["user"] == "omer"
|
695
|
+
"http://users-example.com" + path
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
def app
|
700
|
+
Rack::ReverseProxy.new(dummy_app) do
|
701
|
+
reverse_proxy RequestMatcher
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
it "forwards requests to the calling app when the path is not matched" do
|
706
|
+
get "/"
|
707
|
+
expect(last_response.body).to eq("Dummy App")
|
708
|
+
expect(last_response).to be_ok
|
709
|
+
end
|
710
|
+
|
711
|
+
it "proxies requests when a pattern is matched" do
|
712
|
+
stub_request(:get, "http://users-example.com/users?user=omer").to_return(
|
713
|
+
:body => "User App"
|
714
|
+
)
|
715
|
+
|
716
|
+
get "/test", :user => "mark"
|
717
|
+
expect(last_response.body).to eq("Dummy App")
|
718
|
+
|
719
|
+
get "/users", :user => "omer"
|
720
|
+
expect(last_response.body).to eq("User App")
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
724
|
+
describe "with a matching class that accepts headers" do
|
725
|
+
#:nodoc:
|
726
|
+
class MatcherHeaders
|
727
|
+
def self.match(path, headers)
|
728
|
+
MatcherHeaders.new if path.match(%r{^/test}) &&
|
729
|
+
headers["ACCEPT"] &&
|
730
|
+
headers["ACCEPT"] == "foo.bar"
|
731
|
+
end
|
732
|
+
|
733
|
+
def url(path)
|
734
|
+
"http://example.com" + path
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
def app
|
739
|
+
Rack::ReverseProxy.new(dummy_app) do
|
740
|
+
reverse_proxy MatcherHeaders, nil, :accept_headers => true
|
741
|
+
end
|
742
|
+
end
|
743
|
+
|
744
|
+
it "proxies requests when a pattern is matched and correct headers are passed" do
|
745
|
+
stub_request(:get, "http://example.com/test").to_return(
|
746
|
+
:body => "Proxied App with Headers"
|
747
|
+
)
|
748
|
+
get "/test", {}, "HTTP_ACCEPT" => "foo.bar"
|
749
|
+
expect(last_response.body).to eq("Proxied App with Headers")
|
750
|
+
end
|
751
|
+
|
752
|
+
it "does not proxy requests when a pattern is matched and incorrect headers are passed" do
|
753
|
+
stub_request(:get, "http://example.com/test").to_return(
|
754
|
+
:body => "Proxied App with Headers"
|
755
|
+
)
|
756
|
+
get "/test", {}, "HTTP_ACCEPT" => "bar.foo"
|
757
|
+
expect(last_response.body).not_to eq("Proxied App with Headers")
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
describe "as a rack app", skip: rack_version_less_than_three do
|
763
|
+
it "responds with 404 when the path is not matched" do
|
764
|
+
get "/"
|
765
|
+
expect(last_response).to be_not_found
|
766
|
+
end
|
767
|
+
end
|
768
|
+
end
|