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.util.Arrays;
30  import java.util.Date;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.Set;
34  
35  import org.apache.hc.client5.http.cache.HeaderConstants;
36  import org.apache.hc.client5.http.utils.DateUtils;
37  import org.apache.hc.core5.http.Header;
38  import org.apache.hc.core5.http.HeaderElement;
39  import org.apache.hc.core5.http.HttpHeaders;
40  import org.apache.hc.core5.http.HttpMessage;
41  import org.apache.hc.core5.http.HttpRequest;
42  import org.apache.hc.core5.http.HttpResponse;
43  import org.apache.hc.core5.http.HttpStatus;
44  import org.apache.hc.core5.http.HttpVersion;
45  import org.apache.hc.core5.http.ProtocolVersion;
46  import org.apache.hc.core5.http.message.MessageSupport;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  class ResponseCachingPolicy {
51  
52      private static final String[] AUTH_CACHEABLE_PARAMS = {
53              "s-maxage", HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE, HeaderConstants.PUBLIC
54      };
55  
56      private final static Set<Integer> CACHEABLE_STATUS_CODES =
57              new HashSet<>(Arrays.asList(HttpStatus.SC_OK,
58                      HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
59                      HttpStatus.SC_MULTIPLE_CHOICES,
60                      HttpStatus.SC_MOVED_PERMANENTLY,
61                      HttpStatus.SC_GONE));
62  
63      private static final Logger LOG = LoggerFactory.getLogger(ResponseCachingPolicy.class);
64  
65      private final long maxObjectSizeBytes;
66      private final boolean sharedCache;
67      private final boolean neverCache1_0ResponsesWithQueryString;
68      private final Set<Integer> uncacheableStatusCodes;
69  
70      /**
71       * Define a cache policy that limits the size of things that should be stored
72       * in the cache to a maximum of {@link HttpResponse} bytes in size.
73       *
74       * @param maxObjectSizeBytes the size to limit items into the cache
75       * @param sharedCache whether to behave as a shared cache (true) or a
76       * non-shared/private cache (false)
77       * @param neverCache1_0ResponsesWithQueryString true to never cache HTTP 1.0 responses with a query string, false
78       * to cache if explicit cache headers are found.
79       * @param allow303Caching if this policy is permitted to cache 303 response
80       */
81      public ResponseCachingPolicy(final long maxObjectSizeBytes,
82              final boolean sharedCache,
83              final boolean neverCache1_0ResponsesWithQueryString,
84              final boolean allow303Caching) {
85          this.maxObjectSizeBytes = maxObjectSizeBytes;
86          this.sharedCache = sharedCache;
87          this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
88          if (allow303Caching) {
89              uncacheableStatusCodes = new HashSet<>(Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT));
90          } else {
91              uncacheableStatusCodes = new HashSet<>(Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
92          }
93      }
94  
95      /**
96       * Determines if an HttpResponse can be cached.
97       *
98       * @param httpMethod What type of request was this, a GET, PUT, other?
99       * @param response The origin response
100      * @return {@code true} if response is cacheable
101      */
102     public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
103         boolean cacheable = false;
104 
105         if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)) {
106             if (LOG.isDebugEnabled()) {
107                 LOG.debug("{} method response is not cacheable", httpMethod);
108             }
109             return false;
110         }
111 
112         final int status = response.getCode();
113         if (CACHEABLE_STATUS_CODES.contains(status)) {
114             // these response codes MAY be cached
115             cacheable = true;
116         } else if (uncacheableStatusCodes.contains(status)) {
117             if (LOG.isDebugEnabled()) {
118                 LOG.debug("{} response is not cacheable", status);
119             }
120             return false;
121         } else if (unknownStatusCode(status)) {
122             // a response with an unknown status code MUST NOT be
123             // cached
124             if (LOG.isDebugEnabled()) {
125                 LOG.debug("{} response is unknown", status);
126             }
127             return false;
128         }
129 
130         final Header contentLength = response.getFirstHeader(HttpHeaders.CONTENT_LENGTH);
131         if (contentLength != null) {
132             final long contentLengthValue = Long.parseLong(contentLength.getValue());
133             if (contentLengthValue > this.maxObjectSizeBytes) {
134                 if (LOG.isDebugEnabled()) {
135                     LOG.debug("Response content length exceeds {}", this.maxObjectSizeBytes);
136                 }
137                 return false;
138             }
139         }
140 
141         if (response.countHeaders(HeaderConstants.AGE) > 1) {
142             LOG.debug("Multiple Age headers");
143             return false;
144         }
145 
146         if (response.countHeaders(HeaderConstants.EXPIRES) > 1) {
147             LOG.debug("Multiple Expires headers");
148             return false;
149         }
150 
151         if (response.countHeaders(HttpHeaders.DATE) > 1) {
152             LOG.debug("Multiple Date headers");
153             return false;
154         }
155 
156         final Date date = DateUtils.parseDate(response, HttpHeaders.DATE);
157         if (date == null) {
158             LOG.debug("Invalid / missing Date header");
159             return false;
160         }
161 
162         final Iterator<HeaderElement> it = MessageSupport.iterate(response, HeaderConstants.VARY);
163         while (it.hasNext()) {
164             final HeaderElement elem = it.next();
165             if ("*".equals(elem.getName())) {
166                 if (LOG.isDebugEnabled()) {
167                     LOG.debug("Vary * found");
168                 }
169                 return false;
170             }
171         }
172 
173         if (isExplicitlyNonCacheable(response)) {
174             LOG.debug("Response is explicitly non-cacheable");
175             return false;
176         }
177 
178         return cacheable || isExplicitlyCacheable(response);
179     }
180 
181     private boolean unknownStatusCode(final int status) {
182         if (status >= 100 && status <= 101) {
183             return false;
184         }
185         if (status >= 200 && status <= 206) {
186             return false;
187         }
188         if (status >= 300 && status <= 307) {
189             return false;
190         }
191         if (status >= 400 && status <= 417) {
192             return false;
193         }
194         if (status >= 500 && status <= 505) {
195             return false;
196         }
197         return true;
198     }
199 
200     protected boolean isExplicitlyNonCacheable(final HttpResponse response) {
201         final Iterator<HeaderElement> it = MessageSupport.iterate(response, HeaderConstants.CACHE_CONTROL);
202         while (it.hasNext()) {
203             final HeaderElement elem = it.next();
204             if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elem.getName())
205                     || HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elem.getName())
206                     || (sharedCache && HeaderConstants.PRIVATE.equals(elem.getName()))) {
207                 return true;
208             }
209         }
210         return false;
211     }
212 
213     protected boolean hasCacheControlParameterFrom(final HttpMessage msg, final String[] params) {
214         final Iterator<HeaderElement> it = MessageSupport.iterate(msg, HeaderConstants.CACHE_CONTROL);
215         while (it.hasNext()) {
216             final HeaderElement elem = it.next();
217             for (final String param : params) {
218                 if (param.equalsIgnoreCase(elem.getName())) {
219                     return true;
220                 }
221             }
222         }
223         return false;
224     }
225 
226     protected boolean isExplicitlyCacheable(final HttpResponse response) {
227         if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) {
228             return true;
229         }
230         final String[] cacheableParams = { HeaderConstants.CACHE_CONTROL_MAX_AGE, "s-maxage",
231                 HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE,
232                 HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE,
233                 HeaderConstants.PUBLIC
234         };
235         return hasCacheControlParameterFrom(response, cacheableParams);
236     }
237 
238     /**
239      * Determine if the {@link HttpResponse} gotten from the origin is a
240      * cacheable response.
241      *
242      * @param request the {@link HttpRequest} that generated an origin hit
243      * @param response the {@link HttpResponse} from the origin
244      * @return {@code true} if response is cacheable
245      */
246     public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response) {
247         final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : HttpVersion.DEFAULT;
248         if (version.compareToVersion(HttpVersion.HTTP_1_1) > 0) {
249             if (LOG.isDebugEnabled()) {
250                 LOG.debug("Protocol version {} is non-cacheable", version);
251             }
252             return false;
253         }
254 
255         final String[] uncacheableRequestDirectives = { HeaderConstants.CACHE_CONTROL_NO_STORE };
256         if (hasCacheControlParameterFrom(request,uncacheableRequestDirectives)) {
257             LOG.debug("Response is explcitily non-cacheable per cache control directive");
258             return false;
259         }
260 
261         if (request.getRequestUri().contains("?")) {
262             if (neverCache1_0ResponsesWithQueryString && from1_0Origin(response)) {
263                 LOG.debug("Response is not cacheable as it had a query string");
264                 return false;
265             } else if (!isExplicitlyCacheable(response)) {
266                 LOG.debug("Response is not cacheable as it is missing explicit caching headers");
267                 return false;
268             }
269         }
270 
271         if (expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(response)) {
272             LOG.debug("Expires header less or equal to Date header and no cache control directives");
273             return false;
274         }
275 
276         if (sharedCache) {
277             if (request.countHeaders(HeaderConstants.AUTHORIZATION) > 0
278                     && !hasCacheControlParameterFrom(response, AUTH_CACHEABLE_PARAMS)) {
279                 LOG.debug("Request contains private credentials");
280                 return false;
281             }
282         }
283 
284         final String method = request.getMethod();
285         return isResponseCacheable(method, response);
286     }
287 
288     private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(final HttpResponse response) {
289         if (response.getFirstHeader(HeaderConstants.CACHE_CONTROL) != null) {
290             return false;
291         }
292         final Header expiresHdr = response.getFirstHeader(HeaderConstants.EXPIRES);
293         final Header dateHdr = response.getFirstHeader(HttpHeaders.DATE);
294         if (expiresHdr == null || dateHdr == null) {
295             return false;
296         }
297         final Date expires = DateUtils.parseDate(expiresHdr.getValue());
298         final Date date = DateUtils.parseDate(dateHdr.getValue());
299         if (expires == null || date == null) {
300             return false;
301         }
302         return expires.equals(date) || expires.before(date);
303     }
304 
305     private boolean from1_0Origin(final HttpResponse response) {
306         final Iterator<HeaderElement> it = MessageSupport.iterate(response, HeaderConstants.VIA);
307         while (it.hasNext()) {
308             final HeaderElement elt = it.next();
309             final String proto = elt.toString().split("\\s")[0];
310             if (proto.contains("/")) {
311                 return proto.equals("HTTP/1.0");
312             } else {
313                 return proto.equals("1.0");
314             }
315         }
316         final ProtocolVersion version = response.getVersion() != null ? response.getVersion() : HttpVersion.DEFAULT;
317         return HttpVersion.HTTP_1_0.equals(version);
318     }
319 
320 }