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.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
72
73
74
75
76
77
78
79
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
97
98
99
100
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
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
123
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
240
241
242
243
244
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 }