rack-contrib 0.9.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ module Rack
2
+
3
+ #
4
+ # The Rack::StaticCache middleware automatically adds, removes and modifies
5
+ # stuffs in response headers to facilitiate client and proxy caching for static files
6
+ # that minimizes http requests and improves overall load times for second time visitors.
7
+ #
8
+ # Once a static content is stored in a client/proxy the only way to enforce the browser
9
+ # to fetch the latest content and ignore the cache is to rename the static file.
10
+ #
11
+ # Alternatively, we can add a version number into the URL to the content to bypass
12
+ # the caches. Rack::StaticCache by default handles version numbers in the filename.
13
+ # As an example,
14
+ # http://yoursite.com/images/test-1.0.0.png and http://yoursite.com/images/test-2.0.0.png
15
+ # both reffers to the same image file http://yoursite.com/images/test.png
16
+ #
17
+ # Another way to bypass the cache is adding the version number in a field-value pair in the
18
+ # URL query string. As an example, http://yoursite.com/images/test.png?v=1.0.0
19
+ # In that case, set the option :versioning to false to avoid unneccessary regexp calculations.
20
+ #
21
+ # It's better to keep the current version number in some config file and use it in every static
22
+ # content's URL. So each time we modify our static contents, we just have to change the version
23
+ # number to enforce the browser to fetch the latest content.
24
+ #
25
+ # You can use Rack::Deflater along with Rack::StaticCache for further improvements in page loading time.
26
+ #
27
+ # Examples:
28
+ # use Rack::StaticCache, :urls => ["/images", "/css", "/js", "/documents*"], :root => "statics"
29
+ # will serve all requests beginning with /images, /csss or /js from the
30
+ # directory "statics/images", "statics/css", "statics/js".
31
+ # All the files from these directories will have modified headers to enable client/proxy caching,
32
+ # except the files from the directory "documents". Append a * (star) at the end of the pattern
33
+ # if you want to disable caching for any pattern . In that case, plain static contents will be served with
34
+ # default headers.
35
+ #
36
+ # use Rack::StaticCache, :urls => ["/images"], :duration => 2, :versioning => false
37
+ # will serve all requests begining with /images under the current directory (default for the option :root
38
+ # is current directory). All the contents served will have cache expiration duration set to 2 years in headers
39
+ # (default for :duration is 1 year), and StaticCache will not compute any versioning logics (default for
40
+ # :versioning is true)
41
+ #
42
+
43
+
44
+ class StaticCache
45
+
46
+ def initialize(app, options={})
47
+ @app = app
48
+ @urls = options[:urls]
49
+ @no_cache = {}
50
+ @urls.collect! do |url|
51
+ if url =~ /\*$/
52
+ url.sub!(/\*$/, '')
53
+ @no_cache[url] = 1
54
+ end
55
+ url
56
+ end
57
+ root = options[:root] || Dir.pwd
58
+ @file_server = Rack::File.new(root)
59
+ @cache_duration = options[:duration] || 1
60
+ @versioning_enabled = true
61
+ @versioning_enabled = options[:versioning] unless options[:versioning].nil?
62
+ @duration_in_seconds = self.duration_in_seconds
63
+ @duration_in_words = self.duration_in_words
64
+ end
65
+
66
+ def call(env)
67
+ path = env["PATH_INFO"]
68
+ url = @urls.detect{ |u| path.index(u) == 0 }
69
+ unless url.nil?
70
+ path.sub!(/-[\d.]+([.][a-zA-Z][\w]+)?$/, '\1') if @versioning_enabled
71
+ status, headers, body = @file_server.call(env)
72
+ if @no_cache[url].nil?
73
+ headers['Cache-Control'] ="max-age=#{@duration_in_seconds}, public"
74
+ headers['Expires'] = @duration_in_words
75
+ headers.delete 'Etag'
76
+ headers.delete 'Pragma'
77
+ headers.delete 'Last-Modified'
78
+ end
79
+ [status, headers, body]
80
+ else
81
+ @app.call(env)
82
+ end
83
+ end
84
+
85
+ def duration_in_words
86
+ (Time.now + self.duration_in_seconds).strftime '%a, %d %b %Y %H:%M:%S GMT'
87
+ end
88
+
89
+ def duration_in_seconds
90
+ 60 * 60 * 24 * 365 * @cache_duration
91
+ end
92
+ end
93
+ end
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
 
5
5
  s.name = 'rack-contrib'
6
- s.version = '0.9.2'
7
- s.date = '2009-03-07'
6
+ s.version = '1.0.0'
7
+ s.date = '2010-06-07'
8
8
 
9
9
  s.description = "Contributed Rack Middleware and Utilities"
10
10
  s.summary = "Contributed Rack Middleware and Utilities"
@@ -14,20 +14,24 @@ Gem::Specification.new do |s|
14
14
 
15
15
  # = MANIFEST =
16
16
  s.files = %w[
17
+ AUTHORS
17
18
  COPYING
18
19
  README.rdoc
19
20
  Rakefile
20
21
  lib/rack/contrib.rb
21
22
  lib/rack/contrib/accept_format.rb
23
+ lib/rack/contrib/access.rb
22
24
  lib/rack/contrib/backstage.rb
23
25
  lib/rack/contrib/bounce_favicon.rb
24
26
  lib/rack/contrib/callbacks.rb
25
27
  lib/rack/contrib/config.rb
28
+ lib/rack/contrib/cookies.rb
26
29
  lib/rack/contrib/csshttprequest.rb
27
30
  lib/rack/contrib/deflect.rb
28
- lib/rack/contrib/etag.rb
29
31
  lib/rack/contrib/evil.rb
32
+ lib/rack/contrib/expectation_cascade.rb
30
33
  lib/rack/contrib/garbagecollector.rb
34
+ lib/rack/contrib/host_meta.rb
31
35
  lib/rack/contrib/jsonp.rb
32
36
  lib/rack/contrib/lighttpd_script_name_fix.rb
33
37
  lib/rack/contrib/locale.rb
@@ -39,24 +43,32 @@ Gem::Specification.new do |s|
39
43
  lib/rack/contrib/profiler.rb
40
44
  lib/rack/contrib/relative_redirect.rb
41
45
  lib/rack/contrib/response_cache.rb
46
+ lib/rack/contrib/response_headers.rb
42
47
  lib/rack/contrib/route_exceptions.rb
48
+ lib/rack/contrib/runtime.rb
43
49
  lib/rack/contrib/sendfile.rb
44
50
  lib/rack/contrib/signals.rb
51
+ lib/rack/contrib/simple_endpoint.rb
52
+ lib/rack/contrib/static_cache.rb
45
53
  lib/rack/contrib/time_zone.rb
46
54
  rack-contrib.gemspec
47
55
  test/404.html
48
56
  test/Maintenance.html
57
+ test/documents/test
49
58
  test/mail_settings.rb
50
59
  test/spec_rack_accept_format.rb
60
+ test/spec_rack_access.rb
51
61
  test/spec_rack_backstage.rb
52
62
  test/spec_rack_callbacks.rb
53
63
  test/spec_rack_config.rb
54
64
  test/spec_rack_contrib.rb
65
+ test/spec_rack_cookies.rb
55
66
  test/spec_rack_csshttprequest.rb
56
67
  test/spec_rack_deflect.rb
57
- test/spec_rack_etag.rb
58
68
  test/spec_rack_evil.rb
69
+ test/spec_rack_expectation_cascade.rb
59
70
  test/spec_rack_garbagecollector.rb
71
+ test/spec_rack_host_meta.rb
60
72
  test/spec_rack_jsonp.rb
61
73
  test/spec_rack_lighttpd_script_name_fix.rb
62
74
  test/spec_rack_mailexceptions.rb
@@ -67,7 +79,12 @@ Gem::Specification.new do |s|
67
79
  test/spec_rack_profiler.rb
68
80
  test/spec_rack_relative_redirect.rb
69
81
  test/spec_rack_response_cache.rb
82
+ test/spec_rack_response_headers.rb
83
+ test/spec_rack_runtime.rb
70
84
  test/spec_rack_sendfile.rb
85
+ test/spec_rack_simple_endpoint.rb
86
+ test/spec_rack_static_cache.rb
87
+ test/statics/test
71
88
  ]
72
89
  # = MANIFEST =
73
90
 
@@ -75,7 +92,7 @@ Gem::Specification.new do |s|
75
92
 
76
93
  s.extra_rdoc_files = %w[README.rdoc COPYING]
77
94
  s.add_dependency 'rack', '>= 0.9.1'
78
- s.add_dependency 'test-spec', '~> 0.9.0'
95
+ s.add_development_dependency 'test-spec', '>= 0.9.0'
79
96
  s.add_development_dependency 'tmail', '>= 1.2'
80
97
  s.add_development_dependency 'json', '>= 1.1'
81
98
 
@@ -0,0 +1 @@
1
+ nocache
@@ -0,0 +1,154 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/access'
4
+
5
+ context "Rack::Access" do
6
+
7
+ setup do
8
+ @app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, 'hello'] }
9
+ @mock_addr_1 = '111.111.111.111'
10
+ @mock_addr_2 = '192.168.1.222'
11
+ @mock_addr_localhost = '127.0.0.1'
12
+ @mock_addr_range = '192.168.1.0/24'
13
+ end
14
+
15
+ def mock_env(remote_addr, path = '/')
16
+ Rack::MockRequest.env_for(path, { 'REMOTE_ADDR' => remote_addr })
17
+ end
18
+
19
+ def middleware(options = {})
20
+ Rack::Access.new(@app, options)
21
+ end
22
+
23
+ specify "default configuration should deny non-local requests" do
24
+ app = middleware
25
+ status, headers, body = app.call(mock_env(@mock_addr_1))
26
+ status.should.equal 403
27
+ body.should.equal ''
28
+ end
29
+
30
+ specify "default configuration should allow requests from 127.0.0.1" do
31
+ app = middleware
32
+ status, headers, body = app.call(mock_env(@mock_addr_localhost))
33
+ status.should.equal 200
34
+ body.should.equal 'hello'
35
+ end
36
+
37
+ specify "should allow remote addresses in allow_ipmasking" do
38
+ app = middleware('/' => [@mock_addr_1])
39
+ status, headers, body = app.call(mock_env(@mock_addr_1))
40
+ status.should.equal 200
41
+ body.should.equal 'hello'
42
+ end
43
+
44
+ specify "should deny remote addresses not in allow_ipmasks" do
45
+ app = middleware('/' => [@mock_addr_1])
46
+ status, headers, body = app.call(mock_env(@mock_addr_2))
47
+ status.should.equal 403
48
+ body.should.equal ''
49
+ end
50
+
51
+ specify "should allow remote addresses in allow_ipmasks range" do
52
+ app = middleware('/' => [@mock_addr_range])
53
+ status, headers, body = app.call(mock_env(@mock_addr_2))
54
+ status.should.equal 200
55
+ body.should.equal 'hello'
56
+ end
57
+
58
+ specify "should deny remote addresses not in allow_ipmasks range" do
59
+ app = middleware('/' => [@mock_addr_range])
60
+ status, headers, body = app.call(mock_env(@mock_addr_1))
61
+ status.should.equal 403
62
+ body.should.equal ''
63
+ end
64
+
65
+ specify "should allow remote addresses in one of allow_ipmasking" do
66
+ app = middleware('/' => [@mock_addr_range, @mock_addr_localhost])
67
+
68
+ status, headers, body = app.call(mock_env(@mock_addr_2))
69
+ status.should.equal 200
70
+ body.should.equal 'hello'
71
+
72
+ status, headers, body = app.call(mock_env(@mock_addr_localhost))
73
+ status.should.equal 200
74
+ body.should.equal 'hello'
75
+ end
76
+
77
+ specify "should deny remote addresses not in one of allow_ipmasks" do
78
+ app = middleware('/' => [@mock_addr_range, @mock_addr_localhost])
79
+ status, headers, body = app.call(mock_env(@mock_addr_1))
80
+ status.should.equal 403
81
+ body.should.equal ''
82
+ end
83
+
84
+ specify "handles paths correctly" do
85
+ app = middleware({
86
+ 'http://foo.org/bar' => [@mock_addr_localhost],
87
+ '/foo' => [@mock_addr_localhost],
88
+ '/foo/bar' => [@mock_addr_range, @mock_addr_localhost]
89
+ })
90
+
91
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/"))
92
+ status.should.equal 200
93
+ body.should.equal 'hello'
94
+
95
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/qux"))
96
+ status.should.equal 200
97
+ body.should.equal 'hello'
98
+
99
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo"))
100
+ status.should.equal 403
101
+ body.should.equal ''
102
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo"))
103
+ status.should.equal 200
104
+ body.should.equal 'hello'
105
+
106
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/"))
107
+ status.should.equal 403
108
+ body.should.equal ''
109
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/"))
110
+ status.should.equal 200
111
+ body.should.equal 'hello'
112
+
113
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/bar"))
114
+ status.should.equal 403
115
+ body.should.equal ''
116
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/bar"))
117
+ status.should.equal 200
118
+ body.should.equal 'hello'
119
+ status, headers, body = app.call(mock_env(@mock_addr_2, "/foo/bar"))
120
+ status.should.equal 200
121
+ body.should.equal 'hello'
122
+
123
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/bar/"))
124
+ status.should.equal 403
125
+ body.should.equal ''
126
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/bar/"))
127
+ status.should.equal 200
128
+ body.should.equal 'hello'
129
+
130
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo///bar//quux"))
131
+ status.should.equal 403
132
+ body.should.equal ''
133
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo///bar//quux"))
134
+ status.should.equal 200
135
+ body.should.equal 'hello'
136
+
137
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/quux"))
138
+ status.should.equal 403
139
+ body.should.equal ''
140
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/quux"))
141
+ status.should.equal 200
142
+ body.should.equal 'hello'
143
+
144
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/bar"))
145
+ status.should.equal 200
146
+ body.should.equal 'hello'
147
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/bar").merge('HTTP_HOST' => 'foo.org'))
148
+ status.should.equal 403
149
+ body.should.equal ''
150
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/bar").merge('HTTP_HOST' => 'foo.org'))
151
+ status.should.equal 200
152
+ body.should.equal 'hello'
153
+ end
154
+ end
@@ -0,0 +1,56 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/cookies'
4
+
5
+ context "Rack::Cookies" do
6
+ specify "should be able to read received cookies" do
7
+ app = lambda { |env|
8
+ cookies = env['rack.cookies']
9
+ foo, quux = cookies[:foo], cookies['quux']
10
+ [200, {'Content-Type' => 'text/plain'}, ["foo: #{foo}, quux: #{quux}"]]
11
+ }
12
+ app = Rack::Cookies.new(app)
13
+
14
+ response = Rack::MockRequest.new(app).get('/', 'HTTP_COOKIE' => 'foo=bar;quux=h&m')
15
+ response.body.should.equal('foo: bar, quux: h&m')
16
+ end
17
+
18
+ specify "should be able to set new cookies" do
19
+ app = lambda { |env|
20
+ cookies = env['rack.cookies']
21
+ cookies[:foo] = 'bar'
22
+ cookies['quux'] = 'h&m'
23
+ [200, {'Content-Type' => 'text/plain'}, []]
24
+ }
25
+ app = Rack::Cookies.new(app)
26
+
27
+ response = Rack::MockRequest.new(app).get('/')
28
+ response.headers['Set-Cookie'].should.equal("quux=h%26m; path=/\nfoo=bar; path=/")
29
+ end
30
+
31
+ specify "should be able to set cookie with options" do
32
+ app = lambda { |env|
33
+ cookies = env['rack.cookies']
34
+ cookies['foo'] = { :value => 'bar', :path => '/login', :secure => true }
35
+ [200, {'Content-Type' => 'text/plain'}, []]
36
+ }
37
+ app = Rack::Cookies.new(app)
38
+
39
+ response = Rack::MockRequest.new(app).get('/')
40
+ response.headers['Set-Cookie'].should.equal('foo=bar; path=/login; secure')
41
+ end
42
+
43
+ specify "should be able to delete received cookies" do
44
+ app = lambda { |env|
45
+ cookies = env['rack.cookies']
46
+ cookies.delete(:foo)
47
+ foo, quux = cookies['foo'], cookies[:quux]
48
+ [200, {'Content-Type' => 'text/plain'}, ["foo: #{foo}, quux: #{quux}"]]
49
+ }
50
+ app = Rack::Cookies.new(app)
51
+
52
+ response = Rack::MockRequest.new(app).get('/', 'HTTP_COOKIE' => 'foo=bar;quux=h&m')
53
+ response.body.should.equal('foo: , quux: h&m')
54
+ response.headers['Set-Cookie'].should.equal('foo=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT')
55
+ end
56
+ end
@@ -0,0 +1,72 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/expectation_cascade'
4
+
5
+ context "Rack::ExpectationCascade" do
6
+ specify "with no apps returns a 404 if no expectation header was set" do
7
+ app = Rack::ExpectationCascade.new
8
+ env = {}
9
+ response = app.call(env)
10
+ response[0].should.equal 404
11
+ env.should.equal({})
12
+ end
13
+
14
+ specify "with no apps returns a 417 if expectation header was set" do
15
+ app = Rack::ExpectationCascade.new
16
+ env = {"Expect" => "100-continue"}
17
+ response = app.call(env)
18
+ response[0].should.equal 417
19
+ env.should.equal({"Expect" => "100-continue"})
20
+ end
21
+
22
+ specify "returns first successful response" do
23
+ app = Rack::ExpectationCascade.new do |cascade|
24
+ cascade << lambda { |env| [417, {"Content-Type" => "text/plain"}, []] }
25
+ cascade << lambda { |env| [200, {"Content-Type" => "text/plain"}, ["OK"]] }
26
+ end
27
+ response = app.call({})
28
+ response[0].should.equal 200
29
+ response[2][0].should.equal "OK"
30
+ end
31
+
32
+ specify "expectation is set if it has not been already" do
33
+ app = Rack::ExpectationCascade.new do |cascade|
34
+ cascade << lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Expect: #{env["Expect"]}"]] }
35
+ end
36
+ response = app.call({})
37
+ response[0].should.equal 200
38
+ response[2][0].should.equal "Expect: 100-continue"
39
+ end
40
+
41
+ specify "returns a 404 if no apps where matched and no expectation header was set" do
42
+ app = Rack::ExpectationCascade.new do |cascade|
43
+ cascade << lambda { |env| [417, {"Content-Type" => "text/plain"}, []] }
44
+ end
45
+ response = app.call({})
46
+ response[0].should.equal 404
47
+ response[2][0].should.equal nil
48
+ end
49
+
50
+ specify "returns a 417 if no apps where matched and a expectation header was set" do
51
+ app = Rack::ExpectationCascade.new do |cascade|
52
+ cascade << lambda { |env| [417, {"Content-Type" => "text/plain"}, []] }
53
+ end
54
+ response = app.call({"Expect" => "100-continue"})
55
+ response[0].should.equal 417
56
+ response[2][0].should.equal nil
57
+ end
58
+
59
+ specify "nests expectation cascades" do
60
+ app = Rack::ExpectationCascade.new do |c1|
61
+ c1 << Rack::ExpectationCascade.new do |c2|
62
+ c2 << lambda { |env| [417, {"Content-Type" => "text/plain"}, []] }
63
+ end
64
+ c1 << Rack::ExpectationCascade.new do |c2|
65
+ c2 << lambda { |env| [200, {"Content-Type" => "text/plain"}, ["OK"]] }
66
+ end
67
+ end
68
+ response = app.call({})
69
+ response[0].should.equal 200
70
+ response[2][0].should.equal "OK"
71
+ end
72
+ end