rack-cache 0.3.0 → 0.4

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-cache might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/CHANGES +43 -0
  2. data/README +18 -9
  3. data/Rakefile +1 -14
  4. data/TODO +13 -14
  5. data/doc/configuration.markdown +7 -153
  6. data/doc/faq.markdown +8 -0
  7. data/doc/index.markdown +7 -9
  8. data/example/sinatra/app.rb +25 -0
  9. data/example/sinatra/views/index.erb +44 -0
  10. data/lib/rack/cache.rb +5 -11
  11. data/lib/rack/cache/cachecontrol.rb +193 -0
  12. data/lib/rack/cache/context.rb +190 -52
  13. data/lib/rack/cache/entitystore.rb +10 -4
  14. data/lib/rack/cache/key.rb +52 -0
  15. data/lib/rack/cache/metastore.rb +52 -16
  16. data/lib/rack/cache/options.rb +60 -39
  17. data/lib/rack/cache/request.rb +11 -15
  18. data/lib/rack/cache/response.rb +221 -30
  19. data/lib/rack/cache/storage.rb +1 -2
  20. data/rack-cache.gemspec +9 -15
  21. data/test/cache_test.rb +9 -6
  22. data/test/cachecontrol_test.rb +139 -0
  23. data/test/context_test.rb +251 -169
  24. data/test/entitystore_test.rb +12 -11
  25. data/test/key_test.rb +50 -0
  26. data/test/metastore_test.rb +57 -14
  27. data/test/options_test.rb +11 -0
  28. data/test/request_test.rb +19 -0
  29. data/test/response_test.rb +164 -23
  30. data/test/spec_setup.rb +7 -0
  31. metadata +12 -20
  32. data/doc/events.dot +0 -27
  33. data/lib/rack/cache/config.rb +0 -65
  34. data/lib/rack/cache/config/busters.rb +0 -16
  35. data/lib/rack/cache/config/default.rb +0 -133
  36. data/lib/rack/cache/config/no-cache.rb +0 -13
  37. data/lib/rack/cache/core.rb +0 -299
  38. data/lib/rack/cache/headers.rb +0 -325
  39. data/lib/rack/utils/environment_headers.rb +0 -78
  40. data/test/config_test.rb +0 -66
  41. data/test/core_test.rb +0 -84
  42. data/test/environment_headers_test.rb +0 -69
  43. data/test/headers_test.rb +0 -298
  44. data/test/logging_test.rb +0 -45
data/lib/rack/cache.rb CHANGED
@@ -1,10 +1,5 @@
1
- require 'fileutils'
2
- require 'time'
3
1
  require 'rack'
4
2
 
5
- module Rack #:nodoc:
6
- end
7
-
8
3
  # = HTTP Caching For Rack
9
4
  #
10
5
  # Rack::Cache is suitable as a quick, drop-in component to enable HTTP caching
@@ -15,7 +10,6 @@ end
15
10
  # * Freshness/expiration based caching and validation
16
11
  # * Supports HTTP Vary
17
12
  # * Portable: 100% Ruby / works with any Rack-enabled framework
18
- # * VCL-like configuration language for advanced caching policies
19
13
  # * Disk, memcached, and heap memory storage backends
20
14
  #
21
15
  # === Usage
@@ -32,12 +26,12 @@ end
32
26
  # set :entitystore, 'file:/var/cache/rack'
33
27
  # end
34
28
  # run app
35
- #
36
29
  module Rack::Cache
37
- require 'rack/cache/request'
38
- require 'rack/cache/response'
39
- require 'rack/cache/context'
40
- require 'rack/cache/storage'
30
+ autoload :Request, 'rack/cache/request'
31
+ autoload :Response, 'rack/cache/response'
32
+ autoload :Context, 'rack/cache/context'
33
+ autoload :Storage, 'rack/cache/storage'
34
+ autoload :CacheControl, 'rack/cache/cachecontrol'
41
35
 
42
36
  # Create a new Rack::Cache middleware component that fetches resources from
43
37
  # the specified backend application. The +options+ Hash can be used to
@@ -0,0 +1,193 @@
1
+ module Rack
2
+ module Cache
3
+
4
+ # Parses a Cache-Control header and exposes the directives as a Hash.
5
+ # Directives that do not have values are set to +true+.
6
+ class CacheControl < Hash
7
+ def initialize(value=nil)
8
+ parse(value)
9
+ end
10
+
11
+ # Indicates that the response MAY be cached by any cache, even if it
12
+ # would normally be non-cacheable or cacheable only within a non-
13
+ # shared cache.
14
+ #
15
+ # A response may be considered public without this directive if the
16
+ # private directive is not set and the request does not include an
17
+ # Authorization header.
18
+ def public?
19
+ self['public']
20
+ end
21
+
22
+ # Indicates that all or part of the response message is intended for
23
+ # a single user and MUST NOT be cached by a shared cache. This
24
+ # allows an origin server to state that the specified parts of the
25
+ # response are intended for only one user and are not a valid
26
+ # response for requests by other users. A private (non-shared) cache
27
+ # MAY cache the response.
28
+ #
29
+ # Note: This usage of the word private only controls where the
30
+ # response may be cached, and cannot ensure the privacy of the
31
+ # message content.
32
+ def private?
33
+ self['private']
34
+ end
35
+
36
+ # When set in a response, a cache MUST NOT use the response to satisfy a
37
+ # subsequent request without successful revalidation with the origin
38
+ # server. This allows an origin server to prevent caching even by caches
39
+ # that have been configured to return stale responses to client requests.
40
+ #
41
+ # Note that this does not necessary imply that the response may not be
42
+ # stored by the cache, only that the cache cannot serve it without first
43
+ # making a conditional GET request with the origin server.
44
+ #
45
+ # When set in a request, the server MUST NOT use a cached copy for its
46
+ # response. This has quite different semantics compared to the no-cache
47
+ # directive on responses. When the client specifies no-cache, it causes
48
+ # an end-to-end reload, forcing each cache to update their cached copies.
49
+ def no_cache?
50
+ self['no-cache']
51
+ end
52
+
53
+ # Indicates that the response MUST NOT be stored under any circumstances.
54
+ #
55
+ # The purpose of the no-store directive is to prevent the
56
+ # inadvertent release or retention of sensitive information (for
57
+ # example, on backup tapes). The no-store directive applies to the
58
+ # entire message, and MAY be sent either in a response or in a
59
+ # request. If sent in a request, a cache MUST NOT store any part of
60
+ # either this request or any response to it. If sent in a response,
61
+ # a cache MUST NOT store any part of either this response or the
62
+ # request that elicited it. This directive applies to both non-
63
+ # shared and shared caches. "MUST NOT store" in this context means
64
+ # that the cache MUST NOT intentionally store the information in
65
+ # non-volatile storage, and MUST make a best-effort attempt to
66
+ # remove the information from volatile storage as promptly as
67
+ # possible after forwarding it.
68
+ #
69
+ # The purpose of this directive is to meet the stated requirements
70
+ # of certain users and service authors who are concerned about
71
+ # accidental releases of information via unanticipated accesses to
72
+ # cache data structures. While the use of this directive might
73
+ # improve privacy in some cases, we caution that it is NOT in any
74
+ # way a reliable or sufficient mechanism for ensuring privacy. In
75
+ # particular, malicious or compromised caches might not recognize or
76
+ # obey this directive, and communications networks might be
77
+ # vulnerable to eavesdropping.
78
+ def no_store?
79
+ self['no-store']
80
+ end
81
+
82
+ # The expiration time of an entity MAY be specified by the origin
83
+ # server using the Expires header (see section 14.21). Alternatively,
84
+ # it MAY be specified using the max-age directive in a response. When
85
+ # the max-age cache-control directive is present in a cached response,
86
+ # the response is stale if its current age is greater than the age
87
+ # value given (in seconds) at the time of a new request for that
88
+ # resource. The max-age directive on a response implies that the
89
+ # response is cacheable (i.e., "public") unless some other, more
90
+ # restrictive cache directive is also present.
91
+ #
92
+ # If a response includes both an Expires header and a max-age
93
+ # directive, the max-age directive overrides the Expires header, even
94
+ # if the Expires header is more restrictive. This rule allows an origin
95
+ # server to provide, for a given response, a longer expiration time to
96
+ # an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be
97
+ # useful if certain HTTP/1.0 caches improperly calculate ages or
98
+ # expiration times, perhaps due to desynchronized clocks.
99
+ #
100
+ # Many HTTP/1.0 cache implementations will treat an Expires value that
101
+ # is less than or equal to the response Date value as being equivalent
102
+ # to the Cache-Control response directive "no-cache". If an HTTP/1.1
103
+ # cache receives such a response, and the response does not include a
104
+ # Cache-Control header field, it SHOULD consider the response to be
105
+ # non-cacheable in order to retain compatibility with HTTP/1.0 servers.
106
+ #
107
+ # When the max-age directive is included in the request, it indicates
108
+ # that the client is willing to accept a response whose age is no
109
+ # greater than the specified time in seconds.
110
+ def max_age
111
+ self['max-age'].to_i if key?('max-age')
112
+ end
113
+
114
+ # If a response includes an s-maxage directive, then for a shared
115
+ # cache (but not for a private cache), the maximum age specified by
116
+ # this directive overrides the maximum age specified by either the
117
+ # max-age directive or the Expires header. The s-maxage directive
118
+ # also implies the semantics of the proxy-revalidate directive. i.e.,
119
+ # that the shared cache must not use the entry after it becomes stale
120
+ # to respond to a subsequent request without first revalidating it with
121
+ # the origin server. The s-maxage directive is always ignored by a
122
+ # private cache.
123
+ def shared_max_age
124
+ self['s-maxage'].to_i if key?('s-maxage')
125
+ end
126
+ alias_method :s_maxage, :shared_max_age
127
+
128
+ # Because a cache MAY be configured to ignore a server's specified
129
+ # expiration time, and because a client request MAY include a max-
130
+ # stale directive (which has a similar effect), the protocol also
131
+ # includes a mechanism for the origin server to require revalidation
132
+ # of a cache entry on any subsequent use. When the must-revalidate
133
+ # directive is present in a response received by a cache, that cache
134
+ # MUST NOT use the entry after it becomes stale to respond to a
135
+ # subsequent request without first revalidating it with the origin
136
+ # server. (I.e., the cache MUST do an end-to-end revalidation every
137
+ # time, if, based solely on the origin server's Expires or max-age
138
+ # value, the cached response is stale.)
139
+ #
140
+ # The must-revalidate directive is necessary to support reliable
141
+ # operation for certain protocol features. In all circumstances an
142
+ # HTTP/1.1 cache MUST obey the must-revalidate directive; in
143
+ # particular, if the cache cannot reach the origin server for any
144
+ # reason, it MUST generate a 504 (Gateway Timeout) response.
145
+ #
146
+ # Servers SHOULD send the must-revalidate directive if and only if
147
+ # failure to revalidate a request on the entity could result in
148
+ # incorrect operation, such as a silently unexecuted financial
149
+ # transaction. Recipients MUST NOT take any automated action that
150
+ # violates this directive, and MUST NOT automatically provide an
151
+ # unvalidated copy of the entity if revalidation fails.
152
+ def must_revalidate?
153
+ self['must-revalidate']
154
+ end
155
+
156
+ # The proxy-revalidate directive has the same meaning as the must-
157
+ # revalidate directive, except that it does not apply to non-shared
158
+ # user agent caches. It can be used on a response to an
159
+ # authenticated request to permit the user's cache to store and
160
+ # later return the response without needing to revalidate it (since
161
+ # it has already been authenticated once by that user), while still
162
+ # requiring proxies that service many users to revalidate each time
163
+ # (in order to make sure that each user has been authenticated).
164
+ # Note that such authenticated responses also need the public cache
165
+ # control directive in order to allow them to be cached at all.
166
+ def proxy_revalidate?
167
+ self['proxy-revalidate']
168
+ end
169
+
170
+ def to_s
171
+ bools, vals = [], []
172
+ each do |key,value|
173
+ if value == true
174
+ bools << key
175
+ elsif value
176
+ vals << "#{key}=#{value}"
177
+ end
178
+ end
179
+ (bools.sort + vals.sort).join(', ')
180
+ end
181
+
182
+ private
183
+ def parse(value)
184
+ return if value.nil? || value.empty?
185
+ value.delete(' ').split(',').inject(self) do |hash,part|
186
+ name, value = part.split('=', 2)
187
+ hash[name.downcase] = (value || true) unless name.empty?
188
+ hash
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -1,40 +1,48 @@
1
- require 'rack/cache/config'
2
1
  require 'rack/cache/options'
3
- require 'rack/cache/core'
4
2
  require 'rack/cache/request'
5
3
  require 'rack/cache/response'
6
4
  require 'rack/cache/storage'
7
5
 
8
6
  module Rack::Cache
9
7
  # Implements Rack's middleware interface and provides the context for all
10
- # cache logic. This class includes the Options, Config, and Core modules
11
- # to provide much of its core functionality.
12
-
8
+ # cache logic, including the core logic engine.
13
9
  class Context
14
10
  include Rack::Cache::Options
15
- include Rack::Cache::Config
16
- include Rack::Cache::Core
11
+
12
+ # Array of trace Symbols
13
+ attr_reader :trace
17
14
 
18
15
  # The Rack application object immediately downstream.
19
16
  attr_reader :backend
20
17
 
21
- def initialize(backend, options={}, &block)
22
- @errors = nil
23
- @env = nil
18
+ def initialize(backend, options={})
24
19
  @backend = backend
20
+ @trace = []
21
+
25
22
  initialize_options options
26
- initialize_core
27
- initialize_config(&block)
23
+ yield self if block_given?
24
+
25
+ @private_header_keys =
26
+ private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
28
27
  end
29
28
 
30
- # The call! method is invoked on the duplicate context instance.
31
- # process_request is defined in Core.
32
- alias_method :call!, :process_request
33
- protected :call!
29
+ # The configured MetaStore instance. Changing the rack-cache.metastore
30
+ # value effects the result of this method immediately.
31
+ def metastore
32
+ uri = options['rack-cache.metastore']
33
+ storage.resolve_metastore_uri(uri)
34
+ end
35
+
36
+ # The configured EntityStore instance. Changing the rack-cache.entitystore
37
+ # value effects the result of this method immediately.
38
+ def entitystore
39
+ uri = options['rack-cache.entitystore']
40
+ storage.resolve_entitystore_uri(uri)
41
+ end
34
42
 
35
- # The Rack call interface. The receiver acts as a prototype and runs each
36
- # request in a duplicate object, unless the +rack.run_once+ variable is set
37
- # in the environment.
43
+ # The Rack call interface. The receiver acts as a prototype and runs
44
+ # each request in a dup object unless the +rack.run_once+ variable is
45
+ # set in the environment.
38
46
  def call(env)
39
47
  if env['rack.run_once']
40
48
  call! env
@@ -43,53 +51,183 @@ module Rack::Cache
43
51
  end
44
52
  end
45
53
 
46
- public
47
- # IO-like object that receives log, warning, and error messages;
48
- # defaults to the rack.errors environment variable.
49
- def errors
50
- @errors || (@env && (@errors = @env['rack.errors'])) || STDERR
54
+ # The real Rack call interface. The caching logic is performed within
55
+ # the context of the receiver.
56
+ def call!(env)
57
+ @trace = []
58
+ @env = @default_options.merge(env)
59
+ @request = Request.new(@env.dup.freeze)
60
+
61
+ response =
62
+ if @request.get? || @request.head?
63
+ if !@env['HTTP_EXPECT']
64
+ lookup
65
+ else
66
+ pass
67
+ end
68
+ else
69
+ invalidate
70
+ end
71
+
72
+ # log trace and set X-Rack-Cache tracing header
73
+ trace = @trace.join(', ')
74
+ response.headers['X-Rack-Cache'] = trace
75
+
76
+ # write log message to rack.errors
77
+ if verbose?
78
+ message = "cache: [%s %s] %s\n" %
79
+ [@request.request_method, @request.fullpath, trace]
80
+ @env['rack.errors'].write(message)
81
+ end
82
+
83
+ # tidy up response a bit
84
+ response.not_modified! if not_modified?(response)
85
+ response.body = [] if @request.head?
86
+ response.to_a
51
87
  end
52
88
 
53
- # Set the output stream for log messages, warnings, and errors.
54
- def errors=(ioish)
55
- fail "stream must respond to :write" if ! ioish.respond_to?(:write)
56
- @errors = ioish
89
+ private
90
+
91
+ # Record that an event took place.
92
+ def record(event)
93
+ @trace << event
57
94
  end
58
95
 
59
- # The configured MetaStore instance. Changing the rack-cache.metastore
60
- # environment variable effects the result of this method immediately.
61
- def metastore
62
- uri = options['rack-cache.metastore']
63
- storage.resolve_metastore_uri(uri)
96
+ # Does the request include authorization or other sensitive information
97
+ # that should cause the response to be considered private by default?
98
+ # Private responses are not stored in the cache.
99
+ def private_request?
100
+ @private_header_keys.any? { |key| @env.key?(key) }
64
101
  end
65
102
 
66
- # The configured EntityStore instance. Changing the rack-cache.entitystore
67
- # environment variable effects the result of this method immediately.
68
- def entitystore
69
- uri = options['rack-cache.entitystore']
70
- storage.resolve_entitystore_uri(uri)
103
+ # Determine if the #response validators (ETag, Last-Modified) matches
104
+ # a conditional value specified in #request.
105
+ def not_modified?(response)
106
+ response.etag_matches?(@request.env['HTTP_IF_NONE_MATCH']) ||
107
+ response.last_modified_at?(@request.env['HTTP_IF_MODIFIED_SINCE'])
71
108
  end
72
109
 
73
- protected
74
- # Write a log message to the errors stream. +level+ is a symbol
75
- # such as :error, :warn, :info, or :trace.
76
- def log(level, message=nil, *params)
77
- errors.write("[cache] #{level}: #{message}\n" % params)
78
- errors.flush
110
+ # Whether the cache entry is "fresh enough" to satisfy the request.
111
+ def fresh_enough?(entry)
112
+ if entry.fresh?
113
+ if allow_revalidate? && max_age = @request.cache_control.max_age
114
+ max_age > 0 && max_age >= entry.age
115
+ else
116
+ true
117
+ end
118
+ end
79
119
  end
80
120
 
81
- def info(*message, &bk)
82
- log :info, *message, &bk
121
+ # Delegate the request to the backend and create the response.
122
+ def forward
123
+ Response.new(*backend.call(@env))
83
124
  end
84
125
 
85
- def warn(*message, &bk)
86
- log :warn, *message, &bk
126
+ # The request is sent to the backend, and the backend's response is sent
127
+ # to the client, but is not entered into the cache.
128
+ def pass
129
+ record :pass
130
+ forward
87
131
  end
88
132
 
89
- def trace(*message, &bk)
90
- return unless verbose?
91
- log :trace, *message, &bk
133
+ # Invalidate POST, PUT, DELETE and all methods not understood by this cache
134
+ # See RFC2616 13.10
135
+ def invalidate
136
+ record :invalidate
137
+ metastore.invalidate(@request, entitystore)
138
+ pass
92
139
  end
93
- end
94
140
 
141
+ # Try to serve the response from cache. When a matching cache entry is
142
+ # found and is fresh, use it as the response without forwarding any
143
+ # request to the backend. When a matching cache entry is found but is
144
+ # stale, attempt to #validate the entry with the backend using conditional
145
+ # GET. When no matching cache entry is found, trigger #miss processing.
146
+ def lookup
147
+ if @request.no_cache? && allow_reload?
148
+ record :reload
149
+ fetch
150
+ elsif entry = metastore.lookup(@request, entitystore)
151
+ if fresh_enough?(entry)
152
+ record :fresh
153
+ entry.headers['Age'] = entry.age.to_s
154
+ entry
155
+ else
156
+ record :stale
157
+ validate(entry)
158
+ end
159
+ else
160
+ record :miss
161
+ fetch
162
+ end
163
+ end
164
+
165
+ # Validate that the cache entry is fresh. The original request is used
166
+ # as a template for a conditional GET request with the backend.
167
+ def validate(entry)
168
+ # send no head requests because we want content
169
+ @env['REQUEST_METHOD'] = 'GET'
170
+
171
+ # add our cached validators to the environment
172
+ @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
173
+ @env['HTTP_IF_NONE_MATCH'] = entry.etag
174
+
175
+ backend_response = forward
176
+
177
+ response =
178
+ if backend_response.status == 304
179
+ record :valid
180
+ entry = entry.dup
181
+ entry.headers.delete('Date')
182
+ %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
183
+ next unless value = backend_response.headers[name]
184
+ entry.headers[name] = value
185
+ end
186
+ entry
187
+ else
188
+ record :invalid
189
+ backend_response
190
+ end
191
+
192
+ store(response) if response.cacheable?
193
+
194
+ response
195
+ end
196
+
197
+ # The cache missed or a reload is required. Forward the request to the
198
+ # backend and determine whether the response should be stored.
199
+ def fetch
200
+ # send no head requests because we want content
201
+ @env['REQUEST_METHOD'] = 'GET'
202
+
203
+ # avoid that the backend sends no content
204
+ @env.delete('HTTP_IF_MODIFIED_SINCE')
205
+ @env.delete('HTTP_IF_NONE_MATCH')
206
+
207
+ response = forward
208
+
209
+ # Mark the response as explicitly private if any of the private
210
+ # request headers are present and the response was not explicitly
211
+ # declared public.
212
+ if private_request? && !response.cache_control.public?
213
+ response.private = true
214
+ elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
215
+ # assign a default TTL for the cache entry if none was specified in
216
+ # the response; the must-revalidate cache control directive disables
217
+ # default ttl assigment.
218
+ response.ttl = default_ttl
219
+ end
220
+
221
+ store(response) if response.cacheable?
222
+
223
+ response
224
+ end
225
+
226
+ # Write the response to the cache.
227
+ def store(response)
228
+ record :store
229
+ metastore.store(@request, response, entitystore)
230
+ response.headers['Age'] = response.age.to_s
231
+ end
232
+ end
95
233
  end