View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  import java.time.Instant;
30  import java.util.Iterator;
31  
32  import org.apache.hc.client5.http.cache.HeaderConstants;
33  import org.apache.hc.client5.http.cache.HttpCacheEntry;
34  import org.apache.hc.client5.http.utils.DateUtils;
35  import org.apache.hc.core5.http.Header;
36  import org.apache.hc.core5.http.HeaderElement;
37  import org.apache.hc.core5.http.HttpHost;
38  import org.apache.hc.core5.http.HttpRequest;
39  import org.apache.hc.core5.http.HttpStatus;
40  import org.apache.hc.core5.http.message.MessageSupport;
41  import org.apache.hc.core5.util.TimeValue;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  /**
46   * Determines whether a given {@link HttpCacheEntry} is suitable to be
47   * used as a response for a given {@link HttpRequest}.
48   */
49  class CachedResponseSuitabilityChecker {
50  
51      private static final Logger LOG = LoggerFactory.getLogger(CachedResponseSuitabilityChecker.class);
52  
53      private final boolean sharedCache;
54      private final boolean useHeuristicCaching;
55      private final float heuristicCoefficient;
56      private final TimeValue heuristicDefaultLifetime;
57      private final CacheValidityPolicy validityStrategy;
58  
59      CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
60              final CacheConfig config) {
61          super();
62          this.validityStrategy = validityStrategy;
63          this.sharedCache = config.isSharedCache();
64          this.useHeuristicCaching = config.isHeuristicCachingEnabled();
65          this.heuristicCoefficient = config.getHeuristicCoefficient();
66          this.heuristicDefaultLifetime = config.getHeuristicDefaultLifetime();
67      }
68  
69      CachedResponseSuitabilityChecker(final CacheConfig config) {
70          this(new CacheValidityPolicy(), config);
71      }
72  
73      private boolean isFreshEnough(final HttpCacheEntry entry, final HttpRequest request, final Instant now) {
74          if (validityStrategy.isResponseFresh(entry, now)) {
75              return true;
76          }
77          if (useHeuristicCaching &&
78                  validityStrategy.isResponseHeuristicallyFresh(entry, now, heuristicCoefficient, heuristicDefaultLifetime)) {
79              return true;
80          }
81          if (originInsistsOnFreshness(entry)) {
82              return false;
83          }
84          final long maxStale = getMaxStale(request);
85          if (maxStale == -1) {
86              return false;
87          }
88          return (maxStale > validityStrategy.getStaleness(entry, now).toSeconds());
89      }
90  
91      private boolean originInsistsOnFreshness(final HttpCacheEntry entry) {
92          if (validityStrategy.mustRevalidate(entry)) {
93              return true;
94          }
95          if (!sharedCache) {
96              return false;
97          }
98          return validityStrategy.proxyRevalidate(entry) ||
99              validityStrategy.hasCacheControlDirective(entry, "s-maxage");
100     }
101 
102     private long getMaxStale(final HttpRequest request) {
103         // This is a header value, we leave as-is
104         long maxStale = -1;
105         final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL);
106         while (it.hasNext()) {
107             final HeaderElement elt = it.next();
108             if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
109                 if ((elt.getValue() == null || elt.getValue().trim().isEmpty()) && maxStale == -1) {
110                     maxStale = Long.MAX_VALUE;
111                 } else {
112                     try {
113                         long val = Long.parseLong(elt.getValue());
114                         if (val < 0) {
115                             val = 0;
116                         }
117                         if (maxStale == -1 || val < maxStale) {
118                             maxStale = val;
119                         }
120                     } catch (final NumberFormatException nfe) {
121                         // err on the side of preserving semantic transparency
122                         maxStale = 0;
123                     }
124                 }
125             }
126         }
127         return maxStale;
128     }
129 
130     /**
131      * Determine if I can utilize a {@link HttpCacheEntry} to respond to the given
132      * {@link HttpRequest}
133      *
134      * @param host
135      *            {@link HttpHost}
136      * @param request
137      *            {@link HttpRequest}
138      * @param entry
139      *            {@link HttpCacheEntry}
140      * @param now
141      *            Right now in time
142      * @return boolean yes/no answer
143      */
144     public boolean canCachedResponseBeUsed(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
145         if (!isFreshEnough(entry, request, now)) {
146             LOG.debug("Cache entry is not fresh enough");
147             return false;
148         }
149 
150         if (isGet(request) && !validityStrategy.contentLengthHeaderMatchesActualLength(entry)) {
151             LOG.debug("Cache entry Content-Length and header information do not match");
152             return false;
153         }
154 
155         if (hasUnsupportedConditionalHeaders(request)) {
156             LOG.debug("Request contains unsupported conditional headers");
157             return false;
158         }
159 
160         if (!isConditional(request) && entry.getStatus() == HttpStatus.SC_NOT_MODIFIED) {
161             LOG.debug("Unconditional request and non-modified cached response");
162             return false;
163         }
164 
165         if (isConditional(request) && !allConditionalsMatch(request, entry, now)) {
166             LOG.debug("Conditional request and with mismatched conditions");
167             return false;
168         }
169 
170         if (hasUnsupportedCacheEntryForGet(request, entry)) {
171             LOG.debug("HEAD response caching enabled but the cache entry does not contain a " +
172                       "request method, entity or a 204 response");
173             return false;
174         }
175         final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL);
176         while (it.hasNext()) {
177             final HeaderElement elt = it.next();
178             if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
179                 LOG.debug("Response contained NO CACHE directive, cache was not suitable");
180                 return false;
181             }
182 
183             if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elt.getName())) {
184                 LOG.debug("Response contained NO STORE directive, cache was not suitable");
185                 return false;
186             }
187 
188             if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
189                 try {
190                     // in seconds
191                     final int maxAge = Integer.parseInt(elt.getValue());
192                     if (validityStrategy.getCurrentAge(entry, now).toSeconds() > maxAge) {
193                         LOG.debug("Response from cache was not suitable due to max age");
194                         return false;
195                     }
196                 } catch (final NumberFormatException ex) {
197                     // err conservatively
198                     LOG.debug("Response from cache was malformed: {}", ex.getMessage());
199                     return false;
200                 }
201             }
202 
203             if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
204                 try {
205                     // in seconds
206                     final int maxStale = Integer.parseInt(elt.getValue());
207                     if (validityStrategy.getFreshnessLifetime(entry).toSeconds() > maxStale) {
208                         LOG.debug("Response from cache was not suitable due to max stale freshness");
209                         return false;
210                     }
211                 } catch (final NumberFormatException ex) {
212                     // err conservatively
213                     LOG.debug("Response from cache was malformed: {}", ex.getMessage());
214                     return false;
215                 }
216             }
217 
218             if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())) {
219                 try {
220                     // in seconds
221                     final long minFresh = Long.parseLong(elt.getValue());
222                     if (minFresh < 0L) {
223                         return false;
224                     }
225                     final TimeValue age = validityStrategy.getCurrentAge(entry, now);
226                     final TimeValue freshness = validityStrategy.getFreshnessLifetime(entry);
227                     if (freshness.toSeconds() - age.toSeconds() < minFresh) {
228                         LOG.debug("Response from cache was not suitable due to min fresh " +
229                                 "freshness requirement");
230                         return false;
231                     }
232                 } catch (final NumberFormatException ex) {
233                     // err conservatively
234                     LOG.debug("Response from cache was malformed: {}", ex.getMessage());
235                     return false;
236                 }
237             }
238         }
239 
240         LOG.debug("Response from cache was suitable");
241         return true;
242     }
243 
244     private boolean isGet(final HttpRequest request) {
245         return request.getMethod().equals(HeaderConstants.GET_METHOD);
246     }
247 
248     private boolean entryIsNotA204Response(final HttpCacheEntry entry) {
249         return entry.getStatus() != HttpStatus.SC_NO_CONTENT;
250     }
251 
252     private boolean cacheEntryDoesNotContainMethodAndEntity(final HttpCacheEntry entry) {
253         return entry.getRequestMethod() == null && entry.getResource() == null;
254     }
255 
256     private boolean hasUnsupportedCacheEntryForGet(final HttpRequest request, final HttpCacheEntry entry) {
257         return isGet(request) && cacheEntryDoesNotContainMethodAndEntity(entry) && entryIsNotA204Response(entry);
258     }
259 
260     /**
261      * Is this request the type of conditional request we support?
262      * @param request The current httpRequest being made
263      * @return {@code true} if the request is supported
264      */
265     public boolean isConditional(final HttpRequest request) {
266         return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
267     }
268 
269     /**
270      * Check that conditionals that are part of this request match
271      * @param request The current httpRequest being made
272      * @param entry the cache entry
273      * @param now right NOW in time
274      * @return {@code true} if the request matches all conditionals
275      */
276     public boolean allConditionalsMatch(final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
277         final boolean hasEtagValidator = hasSupportedEtagValidator(request);
278         final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request);
279 
280         final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry);
281         final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now);
282 
283         if ((hasEtagValidator && hasLastModifiedValidator)
284             && !(etagValidatorMatches && lastModifiedValidatorMatches)) {
285             return false;
286         } else if (hasEtagValidator && !etagValidatorMatches) {
287             return false;
288         }
289 
290         return !hasLastModifiedValidator || lastModifiedValidatorMatches;
291     }
292 
293     private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
294         return (request.getFirstHeader(HeaderConstants.IF_RANGE) != null
295                 || request.getFirstHeader(HeaderConstants.IF_MATCH) != null
296                 || hasValidDateField(request, HeaderConstants.IF_UNMODIFIED_SINCE));
297     }
298 
299     private boolean hasSupportedEtagValidator(final HttpRequest request) {
300         return request.containsHeader(HeaderConstants.IF_NONE_MATCH);
301     }
302 
303     private boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
304         return hasValidDateField(request, HeaderConstants.IF_MODIFIED_SINCE);
305     }
306 
307     /**
308      * Check entry against If-None-Match
309      * @param request The current httpRequest being made
310      * @param entry the cache entry
311      * @return boolean does the etag validator match
312      */
313     private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
314         final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
315         final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
316         final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.IF_NONE_MATCH);
317         while (it.hasNext()) {
318             final HeaderElement elt = it.next();
319             final String reqEtag = elt.toString();
320             if (("*".equals(reqEtag) && etag != null) || reqEtag.equals(etag)) {
321                 return true;
322             }
323         }
324         return false;
325     }
326 
327     /**
328      * Check entry against If-Modified-Since, if If-Modified-Since is in the future it is invalid as per
329      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
330      * @param request The current httpRequest being made
331      * @param entry the cache entry
332      * @param now right NOW in time
333      * @return  boolean Does the last modified header match
334      */
335     private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
336         final Instant lastModified = DateUtils.parseStandardDate(entry, HeaderConstants.LAST_MODIFIED);
337         if (lastModified == null) {
338             return false;
339         }
340 
341         for (final Header h : request.getHeaders(HeaderConstants.IF_MODIFIED_SINCE)) {
342             final Instant ifModifiedSince = DateUtils.parseStandardDate(h.getValue());
343             if (ifModifiedSince != null) {
344                 if (ifModifiedSince.isAfter(now) || lastModified.isAfter(ifModifiedSince)) {
345                     return false;
346                 }
347             }
348         }
349         return true;
350     }
351 
352     private boolean hasValidDateField(final HttpRequest request, final String headerName) {
353         for(final Header h : request.getHeaders(headerName)) {
354             final Instant instant = DateUtils.parseStandardDate(h.getValue());
355             return instant != null;
356         }
357         return false;
358     }
359 }