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 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
47
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
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
122 maxStale = 0;
123 }
124 }
125 }
126 }
127 return maxStale;
128 }
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
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
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
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
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
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
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
262
263
264
265 public boolean isConditional(final HttpRequest request) {
266 return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
267 }
268
269
270
271
272
273
274
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
309
310
311
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
329
330
331
332
333
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 }