rack-reverse-proxy 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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