Permalink
Browse files

Move Cache-Control parsing into Rack::Cache::CacheControl class

  • Loading branch information...
rtomayko committed Mar 8, 2009
1 parent 13a55c7 commit d3c73c76488c62ad474391fb00649665a5728cf9
Showing with 362 additions and 157 deletions.
  1. +1 −0 lib/rack/cache.rb
  2. +193 −0 lib/rack/cache/cachecontrol.rb
  3. +5 −5 lib/rack/cache/context.rb
  4. +19 −65 lib/rack/cache/headers.rb
  5. +139 −0 test/cachecontrol_test.rb
  6. +0 −87 test/headers_test.rb
  7. +5 −0 test/spec_setup.rb
View
@@ -32,6 +32,7 @@ module Rack::Cache
require 'rack/cache/response'
require 'rack/cache/context'
require 'rack/cache/storage'
+ require 'rack/cache/cachecontrol'
# Create a new Rack::Cache middleware component that fetches resources from
# the specified backend application. The +options+ Hash can be used to
@@ -0,0 +1,193 @@
+module Rack
+ module Cache
+
+ # Parses a Cache-Control header and exposes the directives as a Hash.
+ # Directives that do not have values are set to +true+.
+ class CacheControl < Hash
+ def initialize(value=nil)
+ parse(value)
+ end
+
+ # Indicates that the response MAY be cached by any cache, even if it
+ # would normally be non-cacheable or cacheable only within a non-
+ # shared cache.
+ #
+ # A response may be considered public without this directive if the
+ # private directive is not set and the request does not include an
+ # Authorization header.
+ def public?
+ self['public']
+ end
+
+ # Indicates that all or part of the response message is intended for
+ # a single user and MUST NOT be cached by a shared cache. This
+ # allows an origin server to state that the specified parts of the
+ # response are intended for only one user and are not a valid
+ # response for requests by other users. A private (non-shared) cache
+ # MAY cache the response.
+ #
+ # Note: This usage of the word private only controls where the
+ # response may be cached, and cannot ensure the privacy of the
+ # message content.
+ def private?
+ self['private']
+ end
+
+ # When set in a response, a cache MUST NOT use the response to satisfy a
+ # subsequent request without successful revalidation with the origin
+ # server. This allows an origin server to prevent caching even by caches
+ # that have been configured to return stale responses to client requests.
+ #
+ # Note that this does not necessary imply that the response may not be
+ # stored by the cache, only that the cache cannot serve it without first
+ # making a conditional GET request with the origin server.
+ #
+ # When set in a request, the server MUST NOT use a cached copy for its
+ # response. This has quite different semantics compared to the no-cache
+ # directive on responses. When the client specifies no-cache, it causes
+ # an end-to-end reload, forcing each cache to update their cached copies.
+ def no_cache?
+ self['no-cache']
+ end
+
+ # Indicates that the response MUST NOT be stored under any circumstances.
+ #
+ # The purpose of the no-store directive is to prevent the
+ # inadvertent release or retention of sensitive information (for
+ # example, on backup tapes). The no-store directive applies to the
+ # entire message, and MAY be sent either in a response or in a
+ # request. If sent in a request, a cache MUST NOT store any part of
+ # either this request or any response to it. If sent in a response,
+ # a cache MUST NOT store any part of either this response or the
+ # request that elicited it. This directive applies to both non-
+ # shared and shared caches. "MUST NOT store" in this context means
+ # that the cache MUST NOT intentionally store the information in
+ # non-volatile storage, and MUST make a best-effort attempt to
+ # remove the information from volatile storage as promptly as
+ # possible after forwarding it.
+ #
+ # The purpose of this directive is to meet the stated requirements
+ # of certain users and service authors who are concerned about
+ # accidental releases of information via unanticipated accesses to
+ # cache data structures. While the use of this directive might
+ # improve privacy in some cases, we caution that it is NOT in any
+ # way a reliable or sufficient mechanism for ensuring privacy. In
+ # particular, malicious or compromised caches might not recognize or
+ # obey this directive, and communications networks might be
+ # vulnerable to eavesdropping.
+ def no_store?
+ self['no-store']
+ end
+
+ # The expiration time of an entity MAY be specified by the origin
+ # server using the Expires header (see section 14.21). Alternatively,
+ # it MAY be specified using the max-age directive in a response. When
+ # the max-age cache-control directive is present in a cached response,
+ # the response is stale if its current age is greater than the age
+ # value given (in seconds) at the time of a new request for that
+ # resource. The max-age directive on a response implies that the
+ # response is cacheable (i.e., "public") unless some other, more
+ # restrictive cache directive is also present.
+ #
+ # If a response includes both an Expires header and a max-age
+ # directive, the max-age directive overrides the Expires header, even
+ # if the Expires header is more restrictive. This rule allows an origin
+ # server to provide, for a given response, a longer expiration time to
+ # an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be
+ # useful if certain HTTP/1.0 caches improperly calculate ages or
+ # expiration times, perhaps due to desynchronized clocks.
+ #
+ # Many HTTP/1.0 cache implementations will treat an Expires value that
+ # is less than or equal to the response Date value as being equivalent
+ # to the Cache-Control response directive "no-cache". If an HTTP/1.1
+ # cache receives such a response, and the response does not include a
+ # Cache-Control header field, it SHOULD consider the response to be
+ # non-cacheable in order to retain compatibility with HTTP/1.0 servers.
+ #
+ # When the max-age directive is included in the request, it indicates
+ # that the client is willing to accept a response whose age is no
+ # greater than the specified time in seconds.
+ def max_age
+ self['max-age'].to_i if key?('max-age')
+ end
+
+ # If a response includes an s-maxage directive, then for a shared
+ # cache (but not for a private cache), the maximum age specified by
+ # this directive overrides the maximum age specified by either the
+ # max-age directive or the Expires header. The s-maxage directive
+ # also implies the semantics of the proxy-revalidate directive. i.e.,
+ # that the shared cache must not use the entry after it becomes stale
+ # to respond to a subsequent request without first revalidating it with
+ # the origin server. The s-maxage directive is always ignored by a
+ # private cache.
+ def shared_max_age
+ self['s-maxage'].to_i if key?('s-maxage')
+ end
+ alias_method :s_maxage, :shared_max_age
+
+ # Because a cache MAY be configured to ignore a server's specified
+ # expiration time, and because a client request MAY include a max-
+ # stale directive (which has a similar effect), the protocol also
+ # includes a mechanism for the origin server to require revalidation
+ # of a cache entry on any subsequent use. When the must-revalidate
+ # directive is present in a response received by a cache, that cache
+ # MUST NOT use the entry after it becomes stale to respond to a
+ # subsequent request without first revalidating it with the origin
+ # server. (I.e., the cache MUST do an end-to-end revalidation every
+ # time, if, based solely on the origin server's Expires or max-age
+ # value, the cached response is stale.)
+ #
+ # The must-revalidate directive is necessary to support reliable
+ # operation for certain protocol features. In all circumstances an
+ # HTTP/1.1 cache MUST obey the must-revalidate directive; in
+ # particular, if the cache cannot reach the origin server for any
+ # reason, it MUST generate a 504 (Gateway Timeout) response.
+ #
+ # Servers SHOULD send the must-revalidate directive if and only if
+ # failure to revalidate a request on the entity could result in
+ # incorrect operation, such as a silently unexecuted financial
+ # transaction. Recipients MUST NOT take any automated action that
+ # violates this directive, and MUST NOT automatically provide an
+ # unvalidated copy of the entity if revalidation fails.
+ def must_revalidate?
+ self['must-revalidate']
+ end
+
+ # The proxy-revalidate directive has the same meaning as the must-
+ # revalidate directive, except that it does not apply to non-shared
+ # user agent caches. It can be used on a response to an
+ # authenticated request to permit the user's cache to store and
+ # later return the response without needing to revalidate it (since
+ # it has already been authenticated once by that user), while still
+ # requiring proxies that service many users to revalidate each time
+ # (in order to make sure that each user has been authenticated).
+ # Note that such authenticated responses also need the public cache
+ # control directive in order to allow them to be cached at all.
+ def proxy_revalidate?
+ self['proxy-revalidate']
+ end
+
+ def to_s
+ bools, vals = [], []
+ each do |key,value|
+ if value == true
+ bools << key
+ elsif value
+ vals << "#{key}=#{value}"
+ end
+ end
+ (bools.sort + vals.sort).join(', ')
+ end
+
+ private
+ def parse(value)
+ return if value.nil? || value.empty?
+ value.delete(' ').split(',').inject(self) do |hash,part|
+ name, value = part.split('=', 2)
+ hash[name.downcase] = (value || true) unless name.empty?
+ hash
+ end
+ end
+ end
+ end
+end
View
@@ -86,7 +86,7 @@ def not_modified?(response)
# Whether the cache entry is "fresh enough" to satisfy the request.
def fresh_enough?(entry)
if entry.fresh?
- if max_age = request.max_age
+ if max_age = request.cache_control.max_age
max_age > 0 && max_age >= entry.age
else
true
@@ -157,7 +157,7 @@ def invalidate
# stale, attempt to #validate the entry with the backend using conditional
# GET. When no matching cache entry is found, trigger #miss processing.
def lookup
- if request.no_cache?
+ if request.cache_control.no_cache?
record :reload
fetch
elsif entry = metastore.lookup(request, entitystore)
@@ -218,12 +218,12 @@ def fetch
response = forward
- # mark the response as explicitly private if any of the private
+ # Mark the response as explicitly private if any of the private
# request headers are present and the response was not explicitly
# declared public.
- if private_request? && !response.public?
+ if private_request? && !response.cache_control.public?
response.private = true
- elsif default_ttl > 0 && response.ttl.nil? && !response.must_revalidate?
+ elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
# assign a default TTL for the cache entry if none was specified in
# the response; the must-revalidate cache control directive disables
# default ttl assigment.
View
@@ -12,45 +12,25 @@ module Headers
# of true. This method always returns a Hash, empty if no Cache-Control
# header is present.
def cache_control
- @cache_control ||=
- headers['Cache-Control'].to_s.delete(' ').split(',').inject({}) {|hash,token|
- name, value = token.split('=', 2)
- hash[name.downcase] = (value || true) unless name.empty?
- hash
- }.freeze
+ @cache_control ||= CacheControl.new(headers['Cache-Control'])
end
# Set the Cache-Control header to the values specified by the Hash. See
# the #cache_control method for information on expected Hash structure.
- def cache_control=(hash)
- value =
- hash.collect { |key,value|
- next nil unless value
- next key if value == true
- "#{key}=#{value}"
- }.compact.join(', ')
- if value.empty?
+ def cache_control=(value)
+ if value.respond_to? :to_hash
+ cache_control.clear
+ cache_control.merge!(value)
+ value = cache_control.to_s
+ end
+
+ if value.nil? || value.empty?
headers.delete('Cache-Control')
- @cache_control = {}
else
headers['Cache-Control'] = value
- @cache_control = hash.dup.freeze
end
end
- # Indicates that the response should not be served from cache without first
- # revalidating with the origin. Note that this does not necessary imply that
- # a caching agent ought not store the response in its cache.
- def no_cache?
- cache_control['no-cache']
- end
-
- # The value of the Cache-Control max-age directive as a Fixnum, or nil
- # when no max-age directive is present.
- def max_age
- age = cache_control['max-age'] && age.to_i
- end
-
# The literal value of the ETag HTTP header or nil if no ETag is specified.
def etag
headers['ETag']
@@ -114,7 +94,7 @@ def fresh?
# validator (Last-Modified, ETag) are considered uncacheable.
def cacheable?
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
- return false if no_store? || private?
+ return false if cache_control.no_store? || cache_control.private?
validateable? || fresh?
end
@@ -124,21 +104,6 @@ def validateable?
headers.key?('Last-Modified') || headers.key?('ETag')
end
- # Indicates that the response should not be stored under any circumstances.
- def no_store?
- cache_control['no-store']
- end
-
- # True when the response has been explicitly marked "public".
- def public?
- cache_control['public']
- end
-
- # True when the response has been marked "private" explicitly.
- def private?
- cache_control['private']
- end
-
# Mark the response "private", making it ineligible for serving other
# clients.
def private=(value)
@@ -152,8 +117,7 @@ def private=(value)
# the TTL of the response should not be overriden to be greater than the
# value provided by the origin.
def must_revalidate?
- cache_control['must-revalidate'] ||
- cache_control['proxy-revalidate']
+ cache_control.must_revalidate || cache_control.proxy_revalidate
end
# The date, as specified by the Date header. When no Date header is present,
@@ -178,11 +142,14 @@ def age
# back on an expires header; return nil when no maximum age can be
# established.
def max_age
- if age = (cache_control['s-maxage'] || cache_control['max-age'])
- age.to_i
- elsif headers['Expires']
- Time.httpdate(headers['Expires']) - date
- end
+ cache_control.shared_max_age ||
+ cache_control.max_age ||
+ (expires && (expires - date))
+ end
+
+ # The value of the Expires header as a Time object.
+ def expires
+ headers['Expires'] && Time.httpdate(headers['Expires'])
end
# The number of seconds after which the response should no longer
@@ -197,19 +164,6 @@ def shared_max_age=(value)
self.cache_control = cache_control.merge('s-maxage' => value.to_s)
end
- # The Time when the response should be considered stale. With a
- # Cache-Control/max-age value is present, this is calculated by adding the
- # number of seconds specified to the responses #date value. Falls back to
- # the time specified in the Expires header or returns nil if neither is
- # present.
- def expires_at
- if max_age = (cache_control['s-maxage'] || cache_control['max-age'])
- date + max_age.to_i
- elsif time = headers['Expires']
- Time.httpdate(time)
- end
- end
-
# The response's time-to-live in seconds, or nil when no freshness
# information is present in the response. When the responses #ttl
# is <= 0, the response may not be served from cache without first
Oops, something went wrong.

0 comments on commit d3c73c7

Please sign in to comment.