rack-contrib 0.9.2 → 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.

Potentially problematic release.


This version of rack-contrib might be problematic. Click here for more details.

@@ -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