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