/usr/lib/ruby/vendor_ruby/rack/cache/context.rb is in ruby-rack-cache 1.2-4.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | require 'rack/cache/options'
require 'rack/cache/request'
require 'rack/cache/response'
require 'rack/cache/storage'
module Rack::Cache
# Implements Rack's middleware interface and provides the context for all
# cache logic, including the core logic engine.
class Context
include Rack::Cache::Options
# Array of trace Symbols
attr_reader :trace
# The Rack application object immediately downstream.
attr_reader :backend
def initialize(backend, options={})
@backend = backend
@trace = []
@env = nil
initialize_options options
yield self if block_given?
@private_header_keys =
private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
end
# The configured MetaStore instance. Changing the rack-cache.metastore
# value effects the result of this method immediately.
def metastore
uri = options['rack-cache.metastore']
storage.resolve_metastore_uri(uri)
end
# The configured EntityStore instance. Changing the rack-cache.entitystore
# value effects the result of this method immediately.
def entitystore
uri = options['rack-cache.entitystore']
storage.resolve_entitystore_uri(uri)
end
# The Rack call interface. The receiver acts as a prototype and runs
# each request in a dup object unless the +rack.run_once+ variable is
# set in the environment.
def call(env)
if env['rack.run_once']
call! env
else
clone.call! env
end
end
# The real Rack call interface. The caching logic is performed within
# the context of the receiver.
def call!(env)
@trace = []
@default_options.each { |k,v| env[k] ||= v }
@env = env
@request = Request.new(@env.dup.freeze)
response =
if @request.get? || @request.head?
if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass']
lookup
else
pass
end
else
invalidate
end
# log trace and set X-Rack-Cache tracing header
trace = @trace.join(', ')
response.headers['X-Rack-Cache'] = trace
# write log message to rack.errors
if verbose?
message = "cache: [%s %s] %s\n" %
[@request.request_method, @request.fullpath, trace]
@env['rack.errors'].write(message)
end
# tidy up response a bit
if (@request.get? || @request.head?) && not_modified?(response)
response.not_modified!
end
if @request.head?
response.body.close if response.body.respond_to?(:close)
response.body = []
end
response.to_a
end
private
# Record that an event took place.
def record(event)
@trace << event
end
# Does the request include authorization or other sensitive information
# that should cause the response to be considered private by default?
# Private responses are not stored in the cache.
def private_request?
@private_header_keys.any? { |key| @env.key?(key) }
end
# Determine if the #response validators (ETag, Last-Modified) matches
# a conditional value specified in #request.
def not_modified?(response)
last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
if etags = @request.env['HTTP_IF_NONE_MATCH']
etags = etags.split(/\s*,\s*/)
(etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
elsif last_modified
response.last_modified == last_modified
end
end
# Whether the cache entry is "fresh enough" to satisfy the request.
def fresh_enough?(entry)
if entry.fresh?
if allow_revalidate? && max_age = @request.cache_control.max_age
max_age > 0 && max_age >= entry.age
else
true
end
end
end
# Delegate the request to the backend and create the response.
def forward
Response.new(*backend.call(@env))
end
# The request is sent to the backend, and the backend's response is sent
# to the client, but is not entered into the cache.
def pass
record :pass
forward
end
# Invalidate POST, PUT, DELETE and all methods not understood by this cache
# See RFC2616 13.10
def invalidate
metastore.invalidate(@request, entitystore)
rescue Exception => e
log_error(e)
pass
else
record :invalidate
pass
end
# Try to serve the response from cache. When a matching cache entry is
# found and is fresh, use it as the response without forwarding any
# request to the backend. When a matching cache entry is found but is
# 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? && allow_reload?
record :reload
fetch
else
begin
entry = metastore.lookup(@request, entitystore)
rescue Exception => e
log_error(e)
return pass
end
if entry
if fresh_enough?(entry)
record :fresh
entry.headers['Age'] = entry.age.to_s
entry
else
record :stale
validate(entry)
end
else
record :miss
fetch
end
end
end
# Validate that the cache entry is fresh. The original request is used
# as a template for a conditional GET request with the backend.
def validate(entry)
# send no head requests because we want content
@env['REQUEST_METHOD'] = 'GET'
# add our cached last-modified validator to the environment
@env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
# Add our cached etag validator to the environment.
# We keep the etags from the client to handle the case when the client
# has a different private valid entry which is not cached here.
cached_etags = entry.etag.to_s.split(/\s*,\s*/)
request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
etags = (cached_etags + request_etags).uniq
@env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')
response = forward
if response.status == 304
record :valid
# Check if the response validated which is not cached here
etag = response.headers['ETag']
return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)
entry = entry.dup
entry.headers.delete('Date')
%w[Date Expires Cache-Control ETag Last-Modified].each do |name|
next unless value = response.headers[name]
entry.headers[name] = value
end
# even though it's empty, be sure to close the response body from upstream
# because middleware use close to signal end of response
response.body.close if response.body.respond_to?(:close)
response = entry
else
record :invalid
end
store(response) if response.cacheable?
response
end
# The cache missed or a reload is required. Forward the request to the
# backend and determine whether the response should be stored. This allows
# conditional / validation requests through to the backend but performs no
# caching of the response when the backend returns a 304.
def fetch
# send no head requests because we want content
@env['REQUEST_METHOD'] = 'GET'
response = forward
# 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.cache_control.public?
response.private = true
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.
response.ttl = default_ttl
end
store(response) if response.cacheable?
response
end
# Write the response to the cache.
def store(response)
strip_ignore_headers(response)
metastore.store(@request, response, entitystore)
response.headers['Age'] = response.age.to_s
rescue Exception => e
log_error(e)
nil
else
record :store
end
# Remove all ignored response headers before writing to the cache.
def strip_ignore_headers(response)
stripped_values = ignore_headers.map { |name| response.headers.delete(name) }
record :ignore if stripped_values.any?
end
def log_error(exception)
@env['rack.errors'].write("cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n")
end
end
end
|