Module: CacheRules
- Extended by:
- CacheRules
- Included in:
- CacheRules, Everything::Around::Me
- Defined in:
- lib/actions.rb,
lib/helpers.rb,
lib/formatting.rb,
lib/cache_rules.rb,
lib/cache_rules.rb,
lib/validations.rb
Overview
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at mozilla.org/MPL/2.0/.
Copyright © 2014-2015 Alexander Williams, Unscramble <[email protected]>
Defined Under Namespace
Modules: Everything
Constant Summary collapse
- HEADERS_NO_CACHE =
%w( Set-Cookie Cookie Accept-Ranges Range If-Range Content-Range Referer From Host Authorization Proxy-Authorization )
- HEADERS_HTTPDATE =
%w( Last-Modified If-Modified-Since If-Unmodified-Since Expires Date X-Cache-Req-Date X-Cache-Res-Date )
- HEADERS_CSV =
%w( Connection Trailer Transfer-Encoding Upgrade Via Accept Accept-Charset Accept-Encoding Accept-Language Allow Content-Encoding Content-Language Vary Cache-Control Warning Pragma If-Match If-None-Match )
- HEADERS_NUMBER =
%w( Age Content-Length Max-Forwards )
- OPTIONS_CACHE =
HEADERS_CSV.select {|header| header == 'Cache-Control' }
- OPTIONS_CSV =
HEADERS_CSV.reject {|header| header == 'Cache-Control' }
- OPTIONS_RETRY =
%w(Retry-After)
- X =
nil
- REQUEST_TABLE =
Decision table for request/cached headers
{ :conditions => { 'cached' => [0, 0, 1, 1, 1, 1, 1, 1, 1], 'must_revalidate' => [X, X, X, X, 0, 0, 0, 1, X], 'no_cache' => [X, X, 0, 0, 0, 0, 0, 0, 1], 'precond_match' => [X, X, 0, 1, 0, 1, X, X, X], 'expired' => [X, X, 0, 0, 1, 1, 1, 1, X], 'only_if_cached' => [0, 1, X, X, X, X, X, X, X], 'allow_stale' => [X, X, X, X, 1, 1, 0, X, X] }, :actions => { 'revalidate' => [X, X, X, X, X, X, X, 1, 1], 'add_age' => [X, X, 1, 1, 1, 1], 'add_x_cache' => %w(MISS MISS HIT HIT STALE STALE EXPIRED), 'add_warning' => [X, X, X, X, '110 - "Response is Stale"', '110 - "Response is Stale"'], 'add_status' => [307, 504, 200, 304, 200, 304, 504], 'return_body' => [X, 'Gateway Timeout', 'cached', X, 'stale', X, 'Gateway Timeout'] } }
- RESPONSE_TABLE =
Decision table for revalidated responses
{ :conditions => { 'is_error' => [0, 0, 1, 1, 1], 'allow_stale' => [X, X, 0, 1, 1], 'validator_match' => [0, 1, X, 0, 1] }, :actions => { 'revalidate' => [], 'add_age' => [1, 1, X, 1, 1], 'add_x_cache' => %w(REVALIDATED REVALIDATED EXPIRED STALE STALE), 'add_warning' => [X, X, X, '111 - "Revalidation Failed"', '111 - "Revalidation Failed"'], 'add_status' => [200, 304, 504, 200, 304], 'return_body' => ['cached', X, 'Gateway Timeout', 'stale'] } }
- REQUEST_MAP =
Build the map tables in advance for faster lookups i.e: O(1)
helper_table_map(REQUEST_TABLE[:conditions])
- RESPONSE_MAP =
helper_table_map(RESPONSE_TABLE[:conditions])
Instance Method Summary collapse
-
#action_add_age(result) ⇒ Object
Generate an age equal to the current cached entry’s age source: tools.ietf.org/html/rfc7234#section-4.
- #action_add_status(result) ⇒ Object
- #action_add_warning(result) ⇒ Object
- #action_add_x_cache(result) ⇒ Object
- #action_return_body(result) ⇒ Object
-
#action_revalidate(result) ⇒ Object
HTTP Header Actions.
-
#cache_control(header = '') ⇒ Object
OPTIMIZE: this regex is copied from JavaScript, could be greatly simplified Returns a Hash with the directive as key, token (or nil), quoted-string (or nil).
-
#cache_control_string(header) ⇒ Object
Parses the Cache-Control header and returns a comma-separated String.
-
#clean ⇒ Object
Intentionally drop these headers to avoid caching them If-Modified-Since should be dropped if the date isn’t valid source: tools.ietf.org/html/rfc7232#section-3.3.
-
#combine ⇒ Object
Combine headers with a comma if the field-names are duplicate.
- #csv(header = '') ⇒ Object
- #csv_string(header) ⇒ Object
-
#format_field ⇒ Object
Returns a Hash, Array, Integer or String based on the supplied arguments.
-
#format_key ⇒ Object
Format the key to look like this: Last-Modified.
-
#format_value(header, stringify = nil) ⇒ Object
Returns the value of the field.
-
#helper_304_rule1(rules) ⇒ Object
“A cache recipient SHOULD generate a 304 (Not Modified) response if…” source: tools.ietf.org/html/rfc7234#section-4.3.2.
- #helper_304_rule2(rules) ⇒ Object
- #helper_304_rule3(rules) ⇒ Object
-
#helper_304_rule4(rules) ⇒ Object
“The presented Last-Modified time is at least 60 seconds before the Date value.” ¯_(ツ)_/¯ source: tools.ietf.org/html/rfc7232#section-2.2.2.
- #helper_apparent_age(response_time, date_value) ⇒ Object
-
#helper_bit_string(num_conditions, index) ⇒ Object
Returns a string of 0s and 1s.
-
#helper_combine_etags(request, cached) ⇒ Object
Combine entity tags if they exist source: tools.ietf.org/html/rfc7234#section-4.3.2.
- #helper_corrected_age_value(response_time, request_time, age_value) ⇒ Object
- #helper_corrected_initial_age(cached, corrected_age_value, apparent_age) ⇒ Object
-
#helper_current_age(now, cached) ⇒ Object
Calculate the current_age of the cached response source: tools.ietf.org/html/rfc7234#section-4.2.3.
-
#helper_etag(request, cached) ⇒ Object
It is not possible for a response’s ETag to contain a “star”, don’t check for it source: tools.ietf.org/html/rfc7232#section-2.3.2.
-
#helper_etag_match(request, cached) ⇒ Object
Must use the ‘weak comparison’ function source: tools.ietf.org/html/rfc7232#section-3.2.
-
#helper_explicit(cached_headers) ⇒ Object
If the expire times are explicitly declared source: tools.ietf.org/html/rfc7234#section-4.2.1.
-
#helper_freshness_lifetime ⇒ Object
Calculate the Freshness Lifetime of the cached response source: tools.ietf.org/html/rfc7234#section-4.2.1.
-
#helper_has_preconditions ⇒ Object
The validators are required for revalidation source: tools.ietf.org/html/rfc7232#section-2.
-
#helper_has_star(header) ⇒ Object
Header can be a String or Array.
-
#helper_headers_200_304 ⇒ Object
Generate the same headers if they exist for 200/304 responses source: tools.ietf.org/html/rfc7232#section-4.1.
-
#helper_heuristic(now, cached, current_age) ⇒ Object
Calculate Heuristic Freshness if there’s no explicit expiration time source: tools.ietf.org/html/rfc7234#section-4.2.2.
-
#helper_is_if_modified_error?(key, value) ⇒ Boolean
Returns a Boolean after trying to parse the If-Modified-Since, or nil.
- #helper_last_modified(request, cached) ⇒ Object
-
#helper_loop_conditions(table) ⇒ Object
Loop through each condition and see if the answer matches.
- #helper_make_request(http, request) ⇒ Object
- #helper_make_request_timer ⇒ Object
- #helper_max_stale ⇒ Object
- #helper_min_fresh ⇒ Object
- #helper_no_cache ⇒ Object
-
#helper_parse_conditions(table) ⇒ Object
Returns the matching column number, or nil.
-
#helper_remove_warning_1xx ⇒ Object
delete 1xx Warning headers source: tools.ietf.org/html/rfc7234#section-4.3.4.
-
#helper_response(url, actions, column, cached, response = {}) ⇒ Object
Returns the response body, code and headers based on the actions results.
- #helper_response_headers ⇒ Object
-
#helper_row_col_hash(conditions) ⇒ Object
Returns a hash representing a row/column, for the table map.
-
#helper_run_action(actions, column, cached) ⇒ Object
Returns an Array of actions to be performed based on the column number.
-
#helper_run_validate ⇒ Object
Returns a bit Array of answers for the conditions.
-
#helper_table_map(conditions) ⇒ Object
Create a map with all possible combinations.
-
#helper_timestamp(request, cached) ⇒ Object
Use the last modified date if it exists source: tools.ietf.org/html/rfc7234#section-4.3.2.
-
#helper_validate_allow_stale(request_headers, cached_headers) ⇒ Object
Don’t allow stale if no-cache or no-store headers exist source: tools.ietf.org/html/rfc7234#section-4.2.4.
- #helper_weak_compare ⇒ Object
- #httpdate(header) ⇒ Object
-
#httpdate_helper(header) ⇒ Object
Correctly parse the 3 Date/Time formats and convert to GMT source: tools.ietf.org/html/rfc7234#section-4.2.
- #httpdate_string(header) ⇒ Object
-
#make_request ⇒ Object
Returns a net/http response Object.
-
#normalize ⇒ Object
Create a normalized Hash of HTTP headers.
-
#normalize_fields ⇒ Object
Normalizes the value (field-value) of each header.
-
#retry_after(header) ⇒ Object
“The value of this field can be either an HTTP-date or a number of seconds…” source: tools.ietf.org/html/rfc7234#section-7.1.3.
- #retry_after_string(header) ⇒ Object
-
#revalidate_response(*args) ⇒ Object
Revalidates a response by fetching headers from the origin server.
-
#to_bit(&predicate) ⇒ Object
HTTP Header Validators.
-
#unnormalize_fields ⇒ Object
Returns a Hash of Strings.
-
#validate(url, request_headers, cached_headers = {}) ⇒ Object
Public: Validate a URL and the request/cached/response headers TODO: validate the required parameters to ensure they are set correctly.
-
#validate_allow_stale?(headers) ⇒ Boolean
Serving Stale Responses source: tools.ietf.org/html/rfc7234#section-4.2.4.
- #validate_cached?(headers) ⇒ Boolean
-
#validate_expired?(headers) ⇒ Boolean
Compare headers to see if the cached request is expired (Freshness) source: tools.ietf.org/html/rfc7234#section-4.2.
- #validate_is_error?(headers) ⇒ Boolean
-
#validate_must_revalidate?(headers) ⇒ Boolean
Response Cache-Control Directives source: tools.ietf.org/html/rfc7234#section-5.2.2.
-
#validate_no_cache?(headers) ⇒ Boolean
Verify if we’re explicitly told not to serve a response without revalidation.
- #validate_only_if_cached?(headers) ⇒ Boolean
-
#validate_precond_match?(headers) ⇒ Boolean
Precedence: If-None-Match (ETag), then If-Modified-Since (Last-Modified) source: tools.ietf.org/html/rfc7232#section-6.
- #validate_validator_match?(headers) ⇒ Boolean
Instance Method Details
#action_add_age(result) ⇒ Object
Generate an age equal to the current cached entry’s age source: tools.ietf.org/html/rfc7234#section-4
18 19 20 21 22 |
# File 'lib/actions.rb', line 18 def action_add_age(result) current_age = helper_current_age Time.now.gmtime.to_i, result[:cached] {'Age' => current_age.to_s} end |
#action_add_status(result) ⇒ Object
32 33 34 |
# File 'lib/actions.rb', line 32 def action_add_status(result) result[:value] end |
#action_add_warning(result) ⇒ Object
28 29 30 |
# File 'lib/actions.rb', line 28 def action_add_warning(result) {'Warning' => result[:value]} end |
#action_add_x_cache(result) ⇒ Object
24 25 26 |
# File 'lib/actions.rb', line 24 def action_add_x_cache(result) {'Cache-Lookup' => result[:value]} end |
#action_return_body(result) ⇒ Object
36 37 38 |
# File 'lib/actions.rb', line 36 def action_return_body(result) result[:value] end |
#action_revalidate(result) ⇒ Object
HTTP Header Actions
12 13 14 |
# File 'lib/actions.rb', line 12 def action_revalidate(result) result[:value] end |
#cache_control(header = '') ⇒ Object
OPTIMIZE: this regex is copied from JavaScript, could be greatly simplified Returns a Hash with the directive as key, token (or nil), quoted-string (or nil)
115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/formatting.rb', line 115 def cache_control(header = '') result = header.scan /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/ result.reduce({}) {|hash, x| hash.merge({ x[0].downcase => { 'token' => x[1], 'quoted_string' => x[2] } }) } end |
#cache_control_string(header) ⇒ Object
Parses the Cache-Control header and returns a comma-separated String
128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/formatting.rb', line 128 def cache_control_string(header) Array(header).map {|x| token = x[1]['token'] quote = x[1]['quoted_string'] directive = x[0] if token && quote.nil? then "#{ directive }=#{ token }" elsif token.nil? && quote then "#{ directive }=\"#{ quote }\"" else directive end }.join ', ' end |
#clean ⇒ Object
Intentionally drop these headers to avoid caching them If-Modified-Since should be dropped if the date isn’t valid source: tools.ietf.org/html/rfc7232#section-3.3
30 31 32 33 34 35 36 |
# File 'lib/formatting.rb', line 30 def clean ->(headers) { Array(headers).reject {|key, value| HEADERS_NO_CACHE.include?(key) || helper_is_if_modified_error?(key, value) || value.nil? || value.empty? } } end |
#combine ⇒ Object
Combine headers with a comma if the field-names are duplicate
39 40 41 42 43 44 45 46 |
# File 'lib/formatting.rb', line 39 def combine ->(headers) { Array(headers).group_by {|h, _| h }.map {|k, v| v = HEADERS_CSV.include?(k) ? v.map {|_, x| x }.join(', ') : v[0][1] # OPTIMIZE [ k, v ] } } end |
#csv(header = '') ⇒ Object
141 142 143 |
# File 'lib/formatting.rb', line 141 def csv(header = '') header.split(',').map(&:strip) end |
#csv_string(header) ⇒ Object
145 146 147 |
# File 'lib/formatting.rb', line 145 def csv_string(header) Array(header).join ', ' end |
#format_field ⇒ Object
Returns a Hash, Array, Integer or String based on the supplied arguments
65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/formatting.rb', line 65 def format_field Proc.new {|key, header, stringify| f = format_value header, stringify value = case key when *HEADERS_HTTPDATE then f.call('httpdate') # => Hash when *OPTIONS_CACHE then f.call('cache_control') # => Array when *OPTIONS_CSV then f.call('csv') # => Array when *OPTIONS_RETRY then f.call('retry_after') # => Hash or Integer else header # => String end [ key, value ] } end |
#format_key ⇒ Object
Format the key to look like this: Last-Modified
20 21 22 23 24 25 |
# File 'lib/formatting.rb', line 20 def format_key Proc.new {|key, value| k = key.downcase == 'etag' ? 'ETag' : key.split('-').map(&:capitalize).join('-') [ k, value ] } end |
#format_value(header, stringify = nil) ⇒ Object
Returns the value of the field
81 82 83 84 85 |
# File 'lib/formatting.rb', line 81 def format_value(header, stringify = nil) Proc.new {|field| stringify ? send("#{ field }_string", header) : send("#{ field }", header) } end |
#helper_304_rule1(rules) ⇒ Object
“A cache recipient SHOULD generate a 304 (Not Modified) response if…” source: tools.ietf.org/html/rfc7234#section-4.3.2
178 179 180 181 |
# File 'lib/helpers.rb', line 178 def helper_304_rule1(rules) rules[:cached_last_modified] && rules[:cached_last_modified] <= rules[:if_modified_since] end |
#helper_304_rule2(rules) ⇒ Object
183 184 185 186 187 |
# File 'lib/helpers.rb', line 183 def helper_304_rule2(rules) rules[:cached_last_modified].nil? && rules[:date_value] && rules[:date_value] <= rules[:if_modified_since] end |
#helper_304_rule3(rules) ⇒ Object
189 190 191 192 193 |
# File 'lib/helpers.rb', line 189 def helper_304_rule3(rules) rules[:date_value].nil? && rules[:cached_last_modified].nil? && rules[:response_time] <= rules[:if_modified_since] end |
#helper_304_rule4(rules) ⇒ Object
“The presented Last-Modified time is at least 60 seconds before the Date value.” ¯_(ツ)_/¯ source: tools.ietf.org/html/rfc7232#section-2.2.2
197 198 199 200 201 |
# File 'lib/helpers.rb', line 197 def helper_304_rule4(rules) rules[:if_modified_since] && rules[:date_value] && rules[:if_modified_since] <= (rules[:date_value] - 60) end |
#helper_apparent_age(response_time, date_value) ⇒ Object
221 222 223 224 225 |
# File 'lib/helpers.rb', line 221 def helper_apparent_age(response_time, date_value) Proc.new { [0, (response_time - date_value)].max } end |
#helper_bit_string(num_conditions, index) ⇒ Object
Returns a string of 0s and 1s
26 27 28 |
# File 'lib/helpers.rb', line 26 def helper_bit_string(num_conditions, index) index.to_s(2).rjust num_conditions, '0' end |
#helper_combine_etags(request, cached) ⇒ Object
Combine entity tags if they exist source: tools.ietf.org/html/rfc7234#section-4.3.2
113 114 115 116 117 |
# File 'lib/helpers.rb', line 113 def (request, cached) return "*" if helper_has_star(request['If-None-Match']) request['If-None-Match'] ? request['If-None-Match'].push(cached['ETag']).uniq.compact.join(', ') : cached['ETag'] end |
#helper_corrected_age_value(response_time, request_time, age_value) ⇒ Object
227 228 229 230 231 232 233 |
# File 'lib/helpers.rb', line 227 def helper_corrected_age_value(response_time, request_time, age_value) Proc.new { # NOTE: It's technically IMPOSSIBLE for response_time to be LOWER THAN request_time response_delay = response_time - request_time age_value + response_delay } end |
#helper_corrected_initial_age(cached, corrected_age_value, apparent_age) ⇒ Object
235 236 237 238 239 240 241 242 243 244 |
# File 'lib/helpers.rb', line 235 def helper_corrected_initial_age(cached, corrected_age_value, apparent_age) Proc.new { if cached['Via'] && cached['Age'] && cached['Via'].none? {|x| x.match /1\.0/ } # corrected_age_value.call [0, corrected_age_value.call].max # safeguard just in case else [apparent_age.call, corrected_age_value.call].max end } end |
#helper_current_age(now, cached) ⇒ Object
Calculate the current_age of the cached response source: tools.ietf.org/html/rfc7234#section-4.2.3
248 249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/helpers.rb', line 248 def helper_current_age(now, cached) date_value = cached['Date']['timestamp'] # Required request_time = cached['X-Cache-Req-Date']['timestamp'] # Required response_time = cached['X-Cache-Res-Date']['timestamp'] # Required age_value = cached['Age'].nil? ? 0 : cached['Age'].to_i apparent_age = helper_apparent_age response_time, date_value corrected_age_value = helper_corrected_age_value response_time, request_time, age_value corrected_initial_age = helper_corrected_initial_age cached, corrected_age_value, apparent_age resident_time = now - response_time corrected_initial_age.call + resident_time end |
#helper_etag(request, cached) ⇒ Object
It is not possible for a response’s ETag to contain a “star”, don’t check for it source: tools.ietf.org/html/rfc7232#section-2.3.2
154 155 156 |
# File 'lib/helpers.rb', line 154 def helper_etag(request, cached) helper_has_star(request['If-None-Match']) || helper_etag_match(request['If-None-Match'], cached['ETag']) end |
#helper_etag_match(request, cached) ⇒ Object
Must use the ‘weak comparison’ function source: tools.ietf.org/html/rfc7232#section-3.2
144 145 146 147 148 149 150 |
# File 'lib/helpers.rb', line 144 def helper_etag_match(request, cached) return unless request && cached request.any? {|x| helper_weak_compare.call(x, cached) } end |
#helper_explicit(cached_headers) ⇒ Object
If the expire times are explicitly declared source: tools.ietf.org/html/rfc7234#section-4.2.1
279 280 281 282 283 284 285 286 |
# File 'lib/helpers.rb', line 279 def helper_explicit(cached_headers) if (( cached = cached_headers['Cache-Control'] )) return cached['s-maxage']['token'] if cached['s-maxage'] return cached['max-age']['token'] if cached['max-age'] end return (cached_headers['Expires']['timestamp'] - cached_headers['Date']['timestamp']) if cached_headers['Expires'] end |
#helper_freshness_lifetime ⇒ Object
Calculate the Freshness Lifetime of the cached response source: tools.ietf.org/html/rfc7234#section-4.2.1
264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/helpers.rb', line 264 def helper_freshness_lifetime now = Time.now.gmtime.to_i ->(cached) { current_age = helper_current_age now, cached # source: https://tools.ietf.org/html/rfc7234#section-4.2 freshness_lifetime = helper_explicit(cached) || helper_heuristic(now, cached, current_age) [freshness_lifetime, current_age] } end |
#helper_has_preconditions ⇒ Object
The validators are required for revalidation source: tools.ietf.org/html/rfc7232#section-2
365 366 367 368 369 |
# File 'lib/helpers.rb', line 365 def helper_has_preconditions Proc.new {|request, cached| request['If-None-Match'] || cached['ETag'] || cached['Last-Modified'] } end |
#helper_has_star(header) ⇒ Object
Header can be a String or Array
107 108 109 |
# File 'lib/helpers.rb', line 107 def helper_has_star(header) header && header.include?("*") end |
#helper_headers_200_304 ⇒ Object
Generate the same headers if they exist for 200/304 responses source: tools.ietf.org/html/rfc7232#section-4.1
89 90 91 92 93 94 95 96 |
# File 'lib/helpers.rb', line 89 def helper_headers_200_304 Proc.new {|cached, response| new_headers = response.select &helper_remove_warning_1xx unnormalize_fields.call cached.merge(new_headers).reject {|key, _| key == 'X-Cache-Req-Date' || key == 'X-Cache-Res-Date' || key == 'Status' } } end |
#helper_heuristic(now, cached, current_age) ⇒ Object
Calculate Heuristic Freshness if there’s no explicit expiration time source: tools.ietf.org/html/rfc7234#section-4.2.2
290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/helpers.rb', line 290 def helper_heuristic(now, cached, current_age) # Use 10% only if there's a Last-Modified header # source: https://tools.ietf.org/html/rfc7234#section-4.2.2 if cached['Last-Modified'] result = (now - cached['Last-Modified']['timestamp']) / 10 # Don't return heuristic responses more than 24 hours old, and avoid sending a 113 Warning ;) # source: https://tools.ietf.org/html/rfc7234#section-4.2.2 current_age > 86400 ? 0 : result else 0 end end |
#helper_is_if_modified_error?(key, value) ⇒ Boolean
Returns a Boolean after trying to parse the If-Modified-Since, or nil
77 78 79 80 81 82 83 84 85 |
# File 'lib/helpers.rb', line 77 def helper_is_if_modified_error?(key, value) if key == 'If-Modified-Since' begin false if DateTime.parse(value) rescue ArgumentError => e true end end end |
#helper_last_modified(request, cached) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/helpers.rb', line 159 def helper_last_modified(request, cached) rules = { :response_time => cached['X-Cache-Res-Date']['timestamp'], # Required :date_value => (cached['Date']['timestamp'] if cached['Date']), :cached_last_modified => (cached['Last-Modified']['timestamp'] if cached['Last-Modified']), :if_modified_since => (request['If-Modified-Since']['timestamp'] if request['If-Modified-Since']) } return unless rules[:if_modified_since] return true if helper_304_rule1(rules) || helper_304_rule2(rules) || helper_304_rule3(rules) || helper_304_rule4(rules) end |
#helper_loop_conditions(table) ⇒ Object
Loop through each condition and see if the answer matches
38 39 40 41 42 43 44 |
# File 'lib/helpers.rb', line 38 def helper_loop_conditions(table) Proc.new {|index| table[:conditions].values[index].map.each_with_index {|x, i| i if x == table[:answers][index] || x.nil? } } end |
#helper_make_request(http, request) ⇒ Object
336 337 338 |
# File 'lib/helpers.rb', line 336 def helper_make_request(http, request) Proc.new { http.request request } end |
#helper_make_request_timer ⇒ Object
340 341 342 343 344 345 346 347 348 |
# File 'lib/helpers.rb', line 340 def helper_make_request_timer Proc.new {|url, request, cached| { :req_date => Time.now.gmtime.httpdate, :res => make_request.call(url, request, cached).call, :res_date => Time.now.gmtime.httpdate } } end |
#helper_max_stale ⇒ Object
305 306 307 308 309 310 311 312 313 314 |
# File 'lib/helpers.rb', line 305 def helper_max_stale ->(request, freshness_lifetime, current_age) { if request && request['max-stale'] token = request['max-stale']['token'] token ? (freshness_lifetime.to_i + token.to_i) > current_age : true else true end } end |
#helper_min_fresh ⇒ Object
317 318 319 320 321 322 323 324 |
# File 'lib/helpers.rb', line 317 def helper_min_fresh Proc.new {|request, freshness_lifetime, current_age| if request && request['min-fresh'] token = request['min-fresh']['token'] freshness_lifetime.to_i >= (current_age + token.to_i) end } end |
#helper_no_cache ⇒ Object
327 328 329 330 331 332 333 334 |
# File 'lib/helpers.rb', line 327 def helper_no_cache Proc.new {|cached_headers| nocache = cached_headers['Cache-Control']['no-cache'] # "If the no-cache response directive specifies one or more field-names..." (nocache && nocache['quoted_string']) && nocache['quoted_string'].split(',').map(&:strip).length > 0 } end |
#helper_parse_conditions(table) ⇒ Object
Returns the matching column number, or nil
31 32 33 34 35 |
# File 'lib/helpers.rb', line 31 def helper_parse_conditions(table) # Loop through each answer and hope to end up with the exact column match result = table[:answers].each_index.map(&helper_loop_conditions(table)).reduce(:&).compact result[0] if result.length == 1 end |
#helper_remove_warning_1xx ⇒ Object
delete 1xx Warning headers source: tools.ietf.org/html/rfc7234#section-4.3.4
100 101 102 103 104 |
# File 'lib/helpers.rb', line 100 def helper_remove_warning_1xx Proc.new {|key, value| {key => value} unless key == 'Warning' && value.reject! {|x| x =~ /^1\d{2}/ } && value.length == 0 } end |
#helper_response(url, actions, column, cached, response = {}) ⇒ Object
Returns the response body, code and headers based on the actions results
64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/helpers.rb', line 64 def helper_response(url, actions, column, cached, response = {}) _, age, x_cache, warning, status, body = helper_run_action actions, column, cached normalized = normalize.call response headers_304 = helper_headers_200_304.call(cached, normalized) if status == 200 || status == 304 headers_url = {'Location' => url} if status == 307 headers = [headers_304, age, warning, x_cache, headers_url].compact.reduce &:merge {:body => body, :code => status, :headers => headers} end |
#helper_response_headers ⇒ Object
350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/helpers.rb', line 350 def helper_response_headers Proc.new {|result| res_headers = normalize.(result[:res].to_hash.map &:flatten) res_headers['Date'] = result[:res_date] if res_headers['Date'] res_headers['X-Cache-Req-Date'] = result[:req_date] res_headers['X-Cache-Res-Date'] = result[:res_date] res_headers['Status'] = result[:res].code res_headers } end |
#helper_row_col_hash(conditions) ⇒ Object
Returns a hash representing a row/column, for the table map
16 17 18 19 20 21 22 23 |
# File 'lib/helpers.rb', line 16 def helper_row_col_hash(conditions) Proc.new {|index| row = helper_bit_string conditions.length, index col = helper_parse_conditions({:conditions => conditions, :answers => row.chars.map(&:to_i)}) {row => col} } end |
#helper_run_action(actions, column, cached) ⇒ Object
Returns an Array of actions to be performed based on the column number
57 58 59 60 61 |
# File 'lib/helpers.rb', line 57 def helper_run_action(actions, column, cached) actions.map {|key, value| send("action_#{ key }", {:value => value[column], :cached => cached}) unless value[column].nil? } end |
#helper_run_validate ⇒ Object
Returns a bit Array of answers for the conditions
47 48 49 50 51 52 53 54 |
# File 'lib/helpers.rb', line 47 def helper_run_validate Proc.new {|table, request, cached, response| table.keys.map {|x| headers = {:request => request, :cached => cached, :response => response} send("validate_#{ x }?", headers) } } end |
#helper_table_map(conditions) ⇒ Object
Create a map with all possible combinations
11 12 13 |
# File 'lib/helpers.rb', line 11 def helper_table_map(conditions) (2**conditions.length).times.map(&helper_row_col_hash(conditions)).reduce(:merge) end |
#helper_timestamp(request, cached) ⇒ Object
Use the last modified date if it exists source: tools.ietf.org/html/rfc7234#section-4.3.2
121 122 123 124 125 |
# File 'lib/helpers.rb', line 121 def (request, cached) return request['If-Modified-Since']['httpdate'] if request['If-Modified-Since'] cached['Last-Modified']['httpdate'] if cached['Last-Modified'] end |
#helper_validate_allow_stale(request_headers, cached_headers) ⇒ Object
Don’t allow stale if no-cache or no-store headers exist source: tools.ietf.org/html/rfc7234#section-4.2.4
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/helpers.rb', line 205 def helper_validate_allow_stale(request_headers, cached_headers) return true if (( request = request_headers['Cache-Control'] )) && ( request['no-cache'] || request['no-store'] ) return true if (( cached = cached_headers['Cache-Control'] )) && ( cached['no-cache'] || cached['no-store'] || cached['must-revalidate'] || cached['s-maxage'] || cached['proxy-revalidate'] ) # Legacy support for HTTP/1.0 Pragma header # source: https://tools.ietf.org/html/rfc7234#section-5.4 return true if request_headers['Pragma'] == 'no-cache' end |
#helper_weak_compare ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/helpers.rb', line 128 def helper_weak_compare etag = /^(W\/)?(\"\w+\")$/ ->(etag1, etag2) { # source: https://tools.ietf.org/html/rfc7232#section-2.3.2 opaque_tag1 = etag.match etag1 opaque_tag2 = etag.match etag2 return false if opaque_tag1.nil? || opaque_tag2.nil? opaque_tag1[2] == opaque_tag2[2] } end |
#httpdate(header) ⇒ Object
87 88 89 90 91 92 93 94 |
# File 'lib/formatting.rb', line 87 def httpdate(header) = httpdate_helper header { 'httpdate' => Time.at().gmtime.httpdate, 'timestamp' => } end |
#httpdate_helper(header) ⇒ Object
Correctly parse the 3 Date/Time formats and convert to GMT source: tools.ietf.org/html/rfc7234#section-4.2
104 105 106 107 108 109 110 111 |
# File 'lib/formatting.rb', line 104 def httpdate_helper(header) # source: https://tools.ietf.org/html/rfc7231#section-7.1.1.1 DateTime.parse(header).to_time.to_i rescue => e # If the supplied date is invalid, use a time in the past (5 minutes ago) # source: https://tools.ietf.org/html/rfc7234#section-5.3 Time.now.gmtime.to_i - 300 end |
#httpdate_string(header) ⇒ Object
96 97 98 99 100 |
# File 'lib/formatting.rb', line 96 def httpdate_string(header) = httpdate_helper header['httpdate'] Time.at().gmtime.httpdate end |
#make_request ⇒ Object
Returns a net/http response Object
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 |
# File 'lib/cache_rules.rb', line 142 def make_request ->(url, request_headers, cached_headers) { uri = URI.parse url http = Net::HTTP.new uri.host, uri.port http.open_timeout = 2 http.read_timeout = 60 http.use_ssl = uri.scheme == 'https' http.verify_mode = OpenSSL::SSL::VERIFY_PEER request = Net::HTTP::Head.new uri.request_uri # Two possible validators: entity tags and timestamp # source: https://tools.ietf.org/html/rfc7234#section-4.3.1 = Proc.new { request_headers, cached_headers }.call = Proc.new { request_headers, cached_headers } # Set the precondition header before making the request request['If-None-Match'] = if ts = .call unless request['If-Modified-Since'] = ts if ts && ! # Make the HTTP(s) request helper_make_request http, request } end |
#normalize ⇒ Object
Create a normalized Hash of HTTP headers
13 14 15 16 17 |
# File 'lib/formatting.rb', line 13 def normalize ->(headers) { Hash[normalize_fields.(combine.(clean.(Array(headers).map &format_key)))] } end |
#normalize_fields ⇒ Object
Normalizes the value (field-value) of each header
49 50 51 52 53 |
# File 'lib/formatting.rb', line 49 def normalize_fields ->(headers) { Array(headers).map &format_field } end |
#retry_after(header) ⇒ Object
“The value of this field can be either an HTTP-date or a number of seconds…” source: tools.ietf.org/html/rfc7234#section-7.1.3
151 152 153 154 155 |
# File 'lib/formatting.rb', line 151 def retry_after(header) Integer(header).abs rescue => e httpdate header end |
#retry_after_string(header) ⇒ Object
157 158 159 |
# File 'lib/formatting.rb', line 157 def retry_after_string(header) header.to_s end |
#revalidate_response(*args) ⇒ Object
Revalidates a response by fetching headers from the origin server
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/cache_rules.rb', line 122 def revalidate_response(*args) url, request, cached = *args has_preconditions = helper_has_preconditions.(request, cached) # 1. get the column column = if has_preconditions res_headers = helper_response_headers.(helper_make_request_timer.(args)) RESPONSE_MAP[helper_run_validate.call(RESPONSE_TABLE[:conditions], request, cached, res_headers).join] else res_headers = {} 2 # return column 2 (504 EXPIRED) end # 2. return the response helper_response url, RESPONSE_TABLE[:actions], column, cached, res_headers rescue => error {:code => 504, :body => 'Gateway Timeout', :headers => [], :error => error., :debug => error} end |
#to_bit(&predicate) ⇒ Object
HTTP Header Validators
Parameters must always be normalized
Return must always be 0 or 1
The If-Match and If-Unmodified-Since conditional header fields are not applicable to a cache. source: tools.ietf.org/html/rfc7234#section-4.3.2
19 20 21 |
# File 'lib/validations.rb', line 19 def to_bit(&predicate) predicate.call ? 1 : 0 end |
#unnormalize_fields ⇒ Object
Returns a Hash of Strings
56 57 58 59 60 61 62 |
# File 'lib/formatting.rb', line 56 def unnormalize_fields ->(headers) { Array(headers).reduce({}) {|hash, (key, value)| hash.merge Hash[[format_field.call(key, value, true)]] } } end |
#validate(url, request_headers, cached_headers = {}) ⇒ Object
Public: Validate a URL and the request/cached/response headers TODO: validate the required parameters to ensure they are set correctly
107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/cache_rules.rb', line 107 def validate(url, request_headers, cached_headers = {}) # 1. normalize the request headers normalized_headers = normalize.call request_headers actions = REQUEST_TABLE[:actions] # 2. get the column matching the request headers column = REQUEST_MAP[helper_run_validate.call(REQUEST_TABLE[:conditions], normalized_headers, cached_headers).join] response = Proc.new { helper_response url, actions, column, cached_headers } revalidate = Proc.new { revalidate_response url, normalized_headers, cached_headers } # 3. return the response or revalidate actions['revalidate'][column] == 1 ? revalidate.call : response.call end |
#validate_allow_stale?(headers) ⇒ Boolean
Serving Stale Responses source: tools.ietf.org/html/rfc7234#section-4.2.4
64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/validations.rb', line 64 def validate_allow_stale?(headers) request, cached = headers.values_at :request, :cached return 0 if cached.length == 0 || helper_validate_allow_stale(request, cached) freshness_lifetime, current_age = helper_freshness_lifetime.call cached max_stale = helper_max_stale.call request['Cache-Control'], freshness_lifetime, current_age min_fresh = helper_min_fresh.call request['Cache-Control'], freshness_lifetime, current_age to_bit { (max_stale && min_fresh != false) || (max_stale.nil? && min_fresh) } end |
#validate_cached?(headers) ⇒ Boolean
23 24 25 |
# File 'lib/validations.rb', line 23 def validate_cached?(headers) to_bit { headers[:cached].length > 0 } end |
#validate_expired?(headers) ⇒ Boolean
Compare headers to see if the cached request is expired (Freshness) source: tools.ietf.org/html/rfc7234#section-4.2
43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/validations.rb', line 43 def validate_expired?(headers) return 0 if headers[:cached].length == 0 freshness_lifetime, current_age = helper_freshness_lifetime.call headers[:cached] response_is_fresh = freshness_lifetime.to_i > current_age return 1 if headers[:cached]['Cache-Control'] && headers[:cached]['Cache-Control']['max-age'] && current_age > headers[:cached]['Cache-Control']['max-age']['token'].to_i to_bit { (response_is_fresh != true) } end |
#validate_is_error?(headers) ⇒ Boolean
120 121 122 |
# File 'lib/validations.rb', line 120 def validate_is_error?(headers) to_bit { headers[:response]['Status'].to_i.between?(500,599) } end |
#validate_must_revalidate?(headers) ⇒ Boolean
Response Cache-Control Directives source: tools.ietf.org/html/rfc7234#section-5.2.2
78 79 80 81 82 83 84 |
# File 'lib/validations.rb', line 78 def validate_must_revalidate?(headers) return 1 if headers[:cached].length == 0 # source: https://tools.ietf.org/html/rfc7234#section-5.2.2.1 # source: https://tools.ietf.org/html/rfc7234#section-5.2.2.7 to_bit { (( cached = headers[:cached]['Cache-Control'] )) && ( cached['must-revalidate'] || cached['proxy-revalidate'] ) } end |
#validate_no_cache?(headers) ⇒ Boolean
Verify if we’re explicitly told not to serve a response without revalidation
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 |
# File 'lib/validations.rb', line 87 def validate_no_cache?(headers) request_headers, cached_headers = headers.values_at :request, :cached return 1 if cached_headers.length == 0 # Must revalidate if this request header exists # source: https://tools.ietf.org/html/rfc7234#section-5.2.1.4 return 1 if (request_headers['Cache-Control'] && request_headers['Cache-Control']['no-cache']) _, current_age = helper_freshness_lifetime.call cached_headers # If max-age is 0 or if the current age is above the max-age # source: https://tools.ietf.org/html/rfc7234#section-5.2.1.1 return 1 if request_headers['Cache-Control'] && request_headers['Cache-Control']['max-age'] && (request_headers['Cache-Control']['max-age']['token'].to_s == "0" || current_age > request_headers['Cache-Control']['max-age']['token'].to_i) # source: https://tools.ietf.org/html/rfc7234#section-5.2.2.2 # source: https://tools.ietf.org/html/rfc7234#section-3.2 if cached_headers['Cache-Control'] return 1 if (( cached = cached_headers['Cache-Control'] )) && helper_no_cache.call(cached_headers) || (cached['no-cache'] && cached['no-cache']['quoted_string'].nil?) || (cached['s-maxage'] && cached['s-maxage']['token'].to_s == "0") || (cached['max-age'] && cached['max-age']['token'].to_s == "0") end # source: https://tools.ietf.org/html/rfc7234#section-5.4 # Legacy support for HTTP/1.0 Pragma header return 1 if request_headers['Pragma'] && request_headers['Pragma']['no-cache'] return 0 end |
#validate_only_if_cached?(headers) ⇒ Boolean
58 59 60 |
# File 'lib/validations.rb', line 58 def validate_only_if_cached?(headers) to_bit { headers[:request]['Cache-Control'] && headers[:request]['Cache-Control']['only-if-cached'] } end |
#validate_precond_match?(headers) ⇒ Boolean
Precedence: If-None-Match (ETag), then If-Modified-Since (Last-Modified) source: tools.ietf.org/html/rfc7232#section-6
29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/validations.rb', line 29 def validate_precond_match?(headers) request, cached = headers.values_at :request, :cached return 0 if cached.length == 0 # Return when the If-None-Match header exists, ignore If-Modified-Since # source: https://tools.ietf.org/html/rfc7232#section-3.3 etag_match = helper_etag(request, cached) return to_bit { etag_match } unless etag_match.nil? to_bit { helper_last_modified(request, cached) } end |
#validate_validator_match?(headers) ⇒ Boolean
124 125 126 127 |
# File 'lib/validations.rb', line 124 def validate_validator_match?(headers) request, response = headers.values_at :request, :response to_bit { response['ETag'] && request['If-None-Match'] && (request['If-None-Match'].include?(response['ETag']) || request['If-None-Match'].include?("*")) } end |