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