rack-reverse-proxy 0.9.1 → 0.10.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.
@@ -0,0 +1,42 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "rack_reverse_proxy/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rack-reverse-proxy"
7
+ spec.version = RackReverseProxy::VERSION
8
+
9
+ spec.authors = [
10
+ "Jon Swope",
11
+ "Ian Ehlert",
12
+ "Roman Ernst",
13
+ "Oleksii Fedorov"
14
+ ]
15
+
16
+ spec.email = [
17
+ "jaswope@gmail.com",
18
+ "ehlertij@gmail.com",
19
+ "rernst@farbenmeer.net",
20
+ "waterlink000@gmail.com"
21
+ ]
22
+
23
+ spec.summary = "A Simple Reverse Proxy for Rack"
24
+ spec.description = <<eos
25
+ A Rack based reverse proxy for basic needs.
26
+ Useful for testing or in cases where webserver configuration is unavailable.
27
+ eos
28
+
29
+ spec.homepage = "https://github.com/waterlink/rack-reverse-proxy"
30
+ spec.license = "MIT"
31
+
32
+ spec.files = `git ls-files -z`.split("\x0")
33
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
34
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_dependency "rack", ">= 1.0.0"
38
+ spec.add_dependency "rack-proxy", "~> 0.5", ">= 0.5.14"
39
+
40
+ spec.add_development_dependency "bundler", "~> 1.7"
41
+ spec.add_development_dependency "rake", "~> 10.3"
42
+ end
@@ -0,0 +1,5 @@
1
+ #/usr/bin/env bash
2
+
3
+ if ruby -e 'exit(1) unless RUBY_VERSION.to_f >= 2.0'; then
4
+ bundle exec rubocop
5
+ fi
@@ -0,0 +1,586 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Rack::ReverseProxy do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Rack::ReverseProxy.new
8
+ end
9
+
10
+ def dummy_app
11
+ lambda { |_| [200, {}, ["Dummy App"]] }
12
+ end
13
+
14
+ let(:http_streaming_response) do
15
+ double(
16
+ "Rack::HttpStreamingResponse",
17
+ :use_ssl= => nil,
18
+ :verify_mode= => nil,
19
+ :headers => {},
20
+ :status => 200,
21
+ :body => "OK"
22
+ )
23
+ end
24
+
25
+ describe "as middleware" do
26
+ def app
27
+ Rack::ReverseProxy.new(dummy_app) do
28
+ reverse_proxy "/test", "http://example.com/", :preserve_host => true
29
+ reverse_proxy "/2test", lambda { |_| "http://example.com/" }
30
+ end
31
+ end
32
+
33
+ it "forwards requests to the calling app when the path is not matched" do
34
+ get "/"
35
+ expect(last_response.body).to eq("Dummy App")
36
+ expect(last_response).to be_ok
37
+ end
38
+
39
+ it "proxies requests when a pattern is matched" do
40
+ stub_request(:get, "http://example.com/test").to_return(:body => "Proxied App")
41
+ get "/test"
42
+ expect(last_response.body).to eq("Proxied App")
43
+ end
44
+
45
+ it "produces a response header of type HeaderHash" do
46
+ stub_request(:get, "http://example.com/test")
47
+ get "/test"
48
+ expect(last_response.headers).to be_an_instance_of(Rack::Utils::HeaderHash)
49
+ end
50
+
51
+ it "parses the headers as a Hash with values of type String" do
52
+ stub_request(:get, "http://example.com/test").to_return(
53
+ :headers => { "cache-control" => "max-age=300, public" }
54
+ )
55
+ get "/test"
56
+ expect(last_response.headers["cache-control"]).to be_an_instance_of(String)
57
+ expect(last_response.headers["cache-control"]).to eq("max-age=300, public")
58
+ end
59
+
60
+ it "proxies requests to a lambda url when a pattern is matched" do
61
+ stub_request(:get, "http://example.com/2test").to_return(:body => "Proxied App2")
62
+ get "/2test"
63
+ expect(last_response.body).to eq("Proxied App2")
64
+ end
65
+
66
+ it "sets the Host header w/o default port" do
67
+ stub_request(:any, "example.com/test/stuff")
68
+ get "/test/stuff"
69
+ expect(
70
+ a_request(:get, "http://example.com/test/stuff").with(
71
+ :headers => { "Host" => "example.com" }
72
+ )
73
+ ).to have_been_made
74
+ end
75
+
76
+ it "sets the X-Forwarded-Host header to the proxying host by default" do
77
+ stub_request(:any, "example.com/test/stuff")
78
+ get "/test/stuff"
79
+ expect(
80
+ a_request(:get, "http://example.com/test/stuff").with(
81
+ :headers => { "X-Forwarded-Host" => "example.org" }
82
+ )
83
+ ).to have_been_made
84
+ end
85
+
86
+ it "does not produce headers with a Status key" do
87
+ stub_request(:get, "http://example.com/2test").to_return(
88
+ :status => 301, :headers => { :status => "301 Moved Permanently" }
89
+ )
90
+
91
+ get "/2test"
92
+
93
+ headers = last_response.headers.to_hash
94
+ expect(headers["Status"]).to be_nil
95
+ end
96
+
97
+ it "formats the headers correctly to avoid duplicates" do
98
+ stub_request(:get, "http://example.com/2test").to_return(
99
+ :headers => { :date => "Wed, 22 Jul 2015 11:27:21 GMT" }
100
+ )
101
+
102
+ get "/2test"
103
+
104
+ headers = last_response.headers.to_hash
105
+ expect(headers["Date"]).to eq("Wed, 22 Jul 2015 11:27:21 GMT")
106
+ expect(headers["date"]).to be_nil
107
+ end
108
+
109
+ it "formats the headers with dashes correctly" do
110
+ stub_request(:get, "http://example.com/2test").to_return(
111
+ :status => 301,
112
+ :headers => { :status => "301 Moved Permanently", :"x-additional-info" => "something" }
113
+ )
114
+
115
+ get "/2test"
116
+
117
+ headers = last_response.headers.to_hash
118
+ expect(headers["X-Additional-Info"]).to eq("something")
119
+ expect(headers["x-additional-info"]).to be_nil
120
+ end
121
+
122
+ it "the response header includes content-length" do
123
+ body = "this is the test body"
124
+ stub_request(:any, "example.com/test/stuff").to_return(
125
+ :body => body, :headers => { "Content-Length" => "10" }
126
+ )
127
+ get "/test/stuff"
128
+ expect(last_response.headers["Content-Length"]).to eq(body.length.to_s)
129
+ end
130
+
131
+ describe "with non-default port" do
132
+ def app
133
+ Rack::ReverseProxy.new(dummy_app) do
134
+ reverse_proxy "/test", "http://example.com:8080/"
135
+ end
136
+ end
137
+
138
+ it "sets the Host header including non-default port" do
139
+ stub_request(:any, "example.com:8080/test/stuff")
140
+ get "/test/stuff"
141
+ expect(
142
+ a_request(:get, "http://example.com:8080/test/stuff").with(
143
+ :headers => { "Host" => "example.com:8080" }
144
+ )
145
+ ).to have_been_made
146
+ end
147
+ end
148
+
149
+ describe "with preserve host turned off" do
150
+ def app
151
+ Rack::ReverseProxy.new(dummy_app) do
152
+ reverse_proxy "/test", "http://example.com/", :preserve_host => false
153
+ end
154
+ end
155
+
156
+ it "does not set the Host header" do
157
+ stub_request(:any, "example.com/test/stuff")
158
+ get "/test/stuff"
159
+
160
+ expect(
161
+ a_request(:get, "http://example.com/test/stuff").with(
162
+ :headers => { "Host" => "example.com" }
163
+ )
164
+ ).not_to have_been_made
165
+
166
+ expect(a_request(:get, "http://example.com/test/stuff")).to have_been_made
167
+ end
168
+ end
169
+
170
+ describe "with x_forwarded_host turned off" do
171
+ def app
172
+ Rack::ReverseProxy.new(dummy_app) do
173
+ reverse_proxy_options :x_forwarded_host => false
174
+ reverse_proxy "/test", "http://example.com/"
175
+ end
176
+ end
177
+
178
+ it "does not set the X-Forwarded-Host header to the proxying host" do
179
+ stub_request(:any, "example.com/test/stuff")
180
+ get "/test/stuff"
181
+ expect(
182
+ a_request(:get, "http://example.com/test/stuff").with(
183
+ :headers => { "X-Forwarded-Host" => "example.org" }
184
+ )
185
+ ).not_to have_been_made
186
+ expect(a_request(:get, "http://example.com/test/stuff")).to have_been_made
187
+ end
188
+ end
189
+
190
+ describe "with timeout configuration" do
191
+ def app
192
+ Rack::ReverseProxy.new(dummy_app) do
193
+ reverse_proxy "/test/slow", "http://example.com/", :timeout => 99
194
+ end
195
+ end
196
+
197
+ it "makes request with basic auth" do
198
+ stub_request(:get, "http://example.com/test/slow")
199
+ allow(Rack::HttpStreamingResponse).to receive(:new).and_return(http_streaming_response)
200
+ expect(http_streaming_response).to receive(:read_timeout=).with(99)
201
+ get "/test/slow"
202
+ end
203
+ end
204
+
205
+ describe "without timeout configuration" do
206
+ def app
207
+ Rack::ReverseProxy.new(dummy_app) do
208
+ reverse_proxy "/test/slow", "http://example.com/"
209
+ end
210
+ end
211
+
212
+ it "makes request with basic auth" do
213
+ stub_request(:get, "http://example.com/test/slow")
214
+ allow(Rack::HttpStreamingResponse).to receive(:new).and_return(http_streaming_response)
215
+ expect(http_streaming_response).not_to receive(:read_timeout=)
216
+ get "/test/slow"
217
+ end
218
+ end
219
+
220
+ describe "with basic auth turned on" do
221
+ def app
222
+ Rack::ReverseProxy.new(dummy_app) do
223
+ reverse_proxy "/test", "http://example.com/", :username => "joe", :password => "shmoe"
224
+ end
225
+ end
226
+
227
+ it "makes request with basic auth" do
228
+ stub_request(:get, "http://joe:shmoe@example.com/test/stuff").to_return(
229
+ :body => "secured content"
230
+ )
231
+ get "/test/stuff"
232
+ expect(last_response.body).to eq("secured content")
233
+ end
234
+ end
235
+
236
+ describe "with preserve response host turned on" do
237
+ def app
238
+ Rack::ReverseProxy.new(dummy_app) do
239
+ reverse_proxy "/test", "http://example.com/", :replace_response_host => true
240
+ end
241
+ end
242
+
243
+ it "replaces the location response header" do
244
+ stub_request(:get, "http://example.com/test/stuff").to_return(
245
+ :headers => { "location" => "http://test.com/bar" }
246
+ )
247
+ get "http://example.com/test/stuff"
248
+ expect(last_response.headers["location"]).to eq("http://example.com/bar")
249
+ end
250
+
251
+ it "keeps the port of the location" do
252
+ stub_request(:get, "http://example.com/test/stuff").to_return(
253
+ :headers => { "location" => "http://test.com/bar" }
254
+ )
255
+ get "http://example.com:3000/test/stuff"
256
+ expect(last_response.headers["location"]).to eq("http://example.com:3000/bar")
257
+ end
258
+
259
+ it "doesn't keep the port when it's default for the protocol" do
260
+ # webmock doesn't allow to stub an https URI, but this is enough to
261
+ # reply to the https code path
262
+ stub_request(:get, "http://example.com/test/stuff").to_return(
263
+ :headers => { "location" => "http://test.com/bar" }
264
+ )
265
+ get "https://example.com/test/stuff"
266
+ expect(last_response.headers["location"]).to eq("https://example.com/bar")
267
+ end
268
+ end
269
+
270
+ describe "with ambiguous routes and all matching" do
271
+ def app
272
+ Rack::ReverseProxy.new(dummy_app) do
273
+ reverse_proxy_options :matching => :all
274
+ reverse_proxy "/test", "http://example.com/"
275
+ reverse_proxy(%r{^/test}, "http://example.com/")
276
+ end
277
+ end
278
+
279
+ it "raises an exception" do
280
+ expect { get "/test" }.to raise_error(RackReverseProxy::Errors::AmbiguousMatch)
281
+ end
282
+ end
283
+
284
+ # FIXME: descriptions are not consistent with examples
285
+ describe "with ambiguous routes and first matching" do
286
+ def app
287
+ Rack::ReverseProxy.new(dummy_app) do
288
+ reverse_proxy_options :matching => :first
289
+ reverse_proxy "/test", "http://example1.com/"
290
+ reverse_proxy(%r{^/test}, "http://example2.com/")
291
+ end
292
+ end
293
+
294
+ it "raises an exception" do
295
+ stub_request(:get, "http://example1.com/test").to_return(:body => "Proxied App")
296
+ get "/test"
297
+ expect(last_response.body).to eq("Proxied App")
298
+ end
299
+ end
300
+
301
+ describe "with force ssl turned on" do
302
+ def app
303
+ Rack::ReverseProxy.new(dummy_app) do
304
+ reverse_proxy "/test", "http://example1.com/",
305
+ :force_ssl => true, :replace_response_host => true
306
+ end
307
+ end
308
+
309
+ it "redirects to the ssl version when requesting non-ssl" do
310
+ stub_request(:get, "http://example1.com/test/stuff").to_return(:body => "proxied")
311
+ get "http://example.com/test/stuff"
312
+ expect(last_response.headers["Location"]).to eq("https://example.com/test/stuff")
313
+ end
314
+
315
+ it "does nothing when already ssl" do
316
+ stub_request(:get, "http://example1.com/test/stuff").to_return(:body => "proxied")
317
+ get "https://example.com/test/stuff"
318
+ expect(last_response.body).to eq("proxied")
319
+ end
320
+ end
321
+
322
+ describe "with a route as a regular expression" do
323
+ def app
324
+ Rack::ReverseProxy.new(dummy_app) do
325
+ reverse_proxy %r{^/test(/.*)$}, "http://example.com$1"
326
+ end
327
+ end
328
+
329
+ it "supports subcaptures" do
330
+ stub_request(:get, "http://example.com/path").to_return(:body => "Proxied App")
331
+ get "/test/path"
332
+ expect(last_response.body).to eq("Proxied App")
333
+ end
334
+ end
335
+
336
+ describe "with a https route" do
337
+ def app
338
+ Rack::ReverseProxy.new(dummy_app) do
339
+ reverse_proxy "/test", "https://example.com"
340
+ end
341
+ end
342
+
343
+ it "makes a secure request" do
344
+ stub_request(:get, "https://example.com/test/stuff").to_return(
345
+ :body => "Proxied Secure App"
346
+ )
347
+ get "/test/stuff"
348
+ expect(last_response.body).to eq("Proxied Secure App")
349
+ end
350
+
351
+ it "sets the Host header w/o default port" do
352
+ stub_request(:any, "https://example.com/test/stuff")
353
+ get "/test/stuff"
354
+ expect(
355
+ a_request(:get, "https://example.com/test/stuff").with(
356
+ :headers => { "Host" => "example.com" }
357
+ )
358
+ ).to have_been_made
359
+ end
360
+ end
361
+
362
+ describe "with a https route on non-default port" do
363
+ def app
364
+ Rack::ReverseProxy.new(dummy_app) do
365
+ reverse_proxy "/test", "https://example.com:8443"
366
+ end
367
+ end
368
+
369
+ it "sets the Host header including non-default port" do
370
+ stub_request(:any, "https://example.com:8443/test/stuff")
371
+ get "/test/stuff"
372
+ expect(
373
+ a_request(:get, "https://example.com:8443/test/stuff").with(
374
+ :headers => { "Host" => "example.com:8443" }
375
+ )
376
+ ).to have_been_made
377
+ end
378
+ end
379
+
380
+ describe "with a route as a string" do
381
+ def app
382
+ Rack::ReverseProxy.new(dummy_app) do
383
+ reverse_proxy "/test", "http://example.com"
384
+ reverse_proxy "/path", "http://example.com/foo$0"
385
+ end
386
+ end
387
+
388
+ it "appends the full path to the uri" do
389
+ stub_request(:get, "http://example.com/test/stuff").to_return(:body => "Proxied App")
390
+ get "/test/stuff"
391
+ expect(last_response.body).to eq("Proxied App")
392
+ end
393
+ end
394
+
395
+ describe "with a generic url" do
396
+ def app
397
+ Rack::ReverseProxy.new(dummy_app) do
398
+ reverse_proxy "/test", "example.com"
399
+ end
400
+ end
401
+
402
+ it "throws an exception" do
403
+ expect { app }.to raise_error(RackReverseProxy::Errors::GenericURI)
404
+ end
405
+ end
406
+
407
+ describe "with a matching route" do
408
+ def app
409
+ Rack::ReverseProxy.new(dummy_app) do
410
+ reverse_proxy "/test", "http://example.com/"
411
+ end
412
+ end
413
+
414
+ %w(get head delete put post).each do |method|
415
+ describe "and using method #{method}" do
416
+ it "forwards the correct request" do
417
+ stub_request(method.to_sym, "http://example.com/test").to_return(
418
+ :body => "Proxied App for #{method}"
419
+ )
420
+ send(method, "/test")
421
+ expect(last_response.body).to eq("Proxied App for #{method}")
422
+ end
423
+
424
+ if %w(put post).include?(method)
425
+ it "forwards the request payload" do
426
+ stub_request(
427
+ method.to_sym,
428
+ "http://example.com/test"
429
+ ).to_return { |req| { :body => req.body } }
430
+ send(method, "/test", :test => "test")
431
+ expect(last_response.body).to eq("test=test")
432
+ end
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ describe "with a matching lambda" do
439
+ def app
440
+ Rack::ReverseProxy.new(dummy_app) do
441
+ reverse_proxy lambda { |path| path.match(%r{^/test}) }, "http://lambda.example.org"
442
+ end
443
+ end
444
+
445
+ it "forwards requests to the calling app when path is not matched" do
446
+ get "/users"
447
+ expect(last_response).to be_ok
448
+ expect(last_response.body).to eq("Dummy App")
449
+ end
450
+
451
+ it "proxies requests when a pattern is matched" do
452
+ stub_request(:get, "http://lambda.example.org/test").to_return(:body => "Proxied App")
453
+
454
+ get "/test"
455
+ expect(last_response.body).to eq("Proxied App")
456
+ end
457
+ end
458
+
459
+ describe "with a matching class" do
460
+ #:nodoc:
461
+ class Matcher
462
+ def self.match(path)
463
+ return unless path =~ %r{^/(test|users)}
464
+ Matcher.new
465
+ end
466
+
467
+ def url(path)
468
+ return "http://users-example.com" + path if path.include?("user")
469
+ "http://example.com" + path
470
+ end
471
+ end
472
+
473
+ def app
474
+ Rack::ReverseProxy.new(dummy_app) do
475
+ reverse_proxy Matcher
476
+ end
477
+ end
478
+
479
+ it "forwards requests to the calling app when the path is not matched" do
480
+ get "/"
481
+ expect(last_response.body).to eq("Dummy App")
482
+ expect(last_response).to be_ok
483
+ end
484
+
485
+ it "proxies requests when a pattern is matched" do
486
+ stub_request(:get, "http://example.com/test").to_return(:body => "Proxied App")
487
+ stub_request(:get, "http://users-example.com/users").to_return(:body => "User App")
488
+
489
+ get "/test"
490
+ expect(last_response.body).to eq("Proxied App")
491
+
492
+ get "/users"
493
+ expect(last_response.body).to eq("User App")
494
+ end
495
+ end
496
+
497
+ describe "with a matching class" do
498
+ #:nodoc:
499
+ class RequestMatcher
500
+ attr_accessor :rackreq
501
+
502
+ def initialize(rackreq)
503
+ self.rackreq = rackreq
504
+ end
505
+
506
+ def self.match(path, _headers, rackreq)
507
+ return nil unless path =~ %r{^/(test|users)}
508
+ RequestMatcher.new(rackreq)
509
+ end
510
+
511
+ def url(path)
512
+ return nil unless rackreq.params["user"] == "omer"
513
+ "http://users-example.com" + path
514
+ end
515
+ end
516
+
517
+ def app
518
+ Rack::ReverseProxy.new(dummy_app) do
519
+ reverse_proxy RequestMatcher
520
+ end
521
+ end
522
+
523
+ it "forwards requests to the calling app when the path is not matched" do
524
+ get "/"
525
+ expect(last_response.body).to eq("Dummy App")
526
+ expect(last_response).to be_ok
527
+ end
528
+
529
+ it "proxies requests when a pattern is matched" do
530
+ stub_request(:get, "http://users-example.com/users?user=omer").to_return(
531
+ :body => "User App"
532
+ )
533
+
534
+ get "/test", :user => "mark"
535
+ expect(last_response.body).to eq("Dummy App")
536
+
537
+ get "/users", :user => "omer"
538
+ expect(last_response.body).to eq("User App")
539
+ end
540
+ end
541
+
542
+ describe "with a matching class that accepts headers" do
543
+ #:nodoc:
544
+ class MatcherHeaders
545
+ def self.match(path, headers)
546
+ if path.match(%r{^/test}) && headers["ACCEPT"] && headers["ACCEPT"] == "foo.bar"
547
+ MatcherHeaders.new
548
+ end
549
+ end
550
+
551
+ def url(path)
552
+ "http://example.com" + path
553
+ end
554
+ end
555
+
556
+ def app
557
+ Rack::ReverseProxy.new(dummy_app) do
558
+ reverse_proxy MatcherHeaders, nil, :accept_headers => true
559
+ end
560
+ end
561
+
562
+ it "proxies requests when a pattern is matched and correct headers are passed" do
563
+ stub_request(:get, "http://example.com/test").to_return(
564
+ :body => "Proxied App with Headers"
565
+ )
566
+ get "/test", {}, "HTTP_ACCEPT" => "foo.bar"
567
+ expect(last_response.body).to eq("Proxied App with Headers")
568
+ end
569
+
570
+ it "does not proxy requests when a pattern is matched and incorrect headers are passed" do
571
+ stub_request(:get, "http://example.com/test").to_return(
572
+ :body => "Proxied App with Headers"
573
+ )
574
+ get "/test", {}, "HTTP_ACCEPT" => "bar.foo"
575
+ expect(last_response.body).not_to eq("Proxied App with Headers")
576
+ end
577
+ end
578
+ end
579
+
580
+ describe "as a rack app" do
581
+ it "responds with 404 when the path is not matched" do
582
+ get "/"
583
+ expect(last_response).to be_not_found
584
+ end
585
+ end
586
+ end