• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package libcore.net.http;
18 
19 import java.net.HttpURLConnection;
20 import java.net.ResponseSource;
21 import java.net.URI;
22 import java.util.Collections;
23 import java.util.Date;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.TreeSet;
28 import java.util.concurrent.TimeUnit;
29 import libcore.util.Objects;
30 
31 /**
32  * Parsed HTTP response headers.
33  */
34 public final class ResponseHeaders {
35 
36     /** HTTP header name for the local time when the request was sent. */
37     private static final String SENT_MILLIS = "X-Android-Sent-Millis";
38 
39     /** HTTP header name for the local time when the response was received. */
40     private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
41 
42     private final URI uri;
43     private final RawHeaders headers;
44 
45     /** The server's time when this response was served, if known. */
46     private Date servedDate;
47 
48     /** The last modified date of the response, if known. */
49     private Date lastModified;
50 
51     /**
52      * The expiration date of the response, if known. If both this field and the
53      * max age are set, the max age is preferred.
54      */
55     private Date expires;
56 
57     /**
58      * Extension header set by HttpURLConnectionImpl specifying the timestamp
59      * when the HTTP request was first initiated.
60      */
61     private long sentRequestMillis;
62 
63     /**
64      * Extension header set by HttpURLConnectionImpl specifying the timestamp
65      * when the HTTP response was first received.
66      */
67     private long receivedResponseMillis;
68 
69     /**
70      * In the response, this field's name "no-cache" is misleading. It doesn't
71      * prevent us from caching the response; it only means we have to validate
72      * the response with the origin server before returning it. We can do this
73      * with a conditional get.
74      */
75     private boolean noCache;
76 
77     /** If true, this response should not be cached. */
78     private boolean noStore;
79 
80     /**
81      * The duration past the response's served date that it can be served
82      * without validation.
83      */
84     private int maxAgeSeconds = -1;
85 
86     /**
87      * The "s-maxage" directive is the max age for shared caches. Not to be
88      * confused with "max-age" for non-shared caches, As in Firefox and Chrome,
89      * this directive is not honored by this cache.
90      */
91     private int sMaxAgeSeconds = -1;
92 
93     /**
94      * This request header field's name "only-if-cached" is misleading. It
95      * actually means "do not use the network". It is set by a client who only
96      * wants to make a request if it can be fully satisfied by the cache.
97      * Cached responses that would require validation (ie. conditional gets) are
98      * not permitted if this header is set.
99      */
100     private boolean isPublic;
101     private boolean mustRevalidate;
102     private String etag;
103     private int ageSeconds = -1;
104 
105     /** Case-insensitive set of field names. */
106     private Set<String> varyFields = Collections.emptySet();
107 
108     private String contentEncoding;
109     private String transferEncoding;
110     private int contentLength = -1;
111     private String connection;
112 
ResponseHeaders(URI uri, RawHeaders headers)113     public ResponseHeaders(URI uri, RawHeaders headers) {
114         this.uri = uri;
115         this.headers = headers;
116 
117         HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
118             @Override public void handle(String directive, String parameter) {
119                 if (directive.equalsIgnoreCase("no-cache")) {
120                     noCache = true;
121                 } else if (directive.equalsIgnoreCase("no-store")) {
122                     noStore = true;
123                 } else if (directive.equalsIgnoreCase("max-age")) {
124                     maxAgeSeconds = HeaderParser.parseSeconds(parameter);
125                 } else if (directive.equalsIgnoreCase("s-maxage")) {
126                     sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
127                 } else if (directive.equalsIgnoreCase("public")) {
128                     isPublic = true;
129                 } else if (directive.equalsIgnoreCase("must-revalidate")) {
130                     mustRevalidate = true;
131                 }
132             }
133         };
134 
135         for (int i = 0; i < headers.length(); i++) {
136             String fieldName = headers.getFieldName(i);
137             String value = headers.getValue(i);
138             if ("Cache-Control".equalsIgnoreCase(fieldName)) {
139                 HeaderParser.parseCacheControl(value, handler);
140             } else if ("Date".equalsIgnoreCase(fieldName)) {
141                 servedDate = HttpDate.parse(value);
142             } else if ("Expires".equalsIgnoreCase(fieldName)) {
143                 expires = HttpDate.parse(value);
144             } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
145                 lastModified = HttpDate.parse(value);
146             } else if ("ETag".equalsIgnoreCase(fieldName)) {
147                 etag = value;
148             } else if ("Pragma".equalsIgnoreCase(fieldName)) {
149                 if (value.equalsIgnoreCase("no-cache")) {
150                     noCache = true;
151                 }
152             } else if ("Age".equalsIgnoreCase(fieldName)) {
153                 ageSeconds = HeaderParser.parseSeconds(value);
154             } else if ("Vary".equalsIgnoreCase(fieldName)) {
155                 // Replace the immutable empty set with something we can mutate.
156                 if (varyFields.isEmpty()) {
157                     varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
158                 }
159                 for (String varyField : value.split(",")) {
160                     varyFields.add(varyField.trim());
161                 }
162             } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
163                 contentEncoding = value;
164             } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
165                 transferEncoding = value;
166             } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
167                 try {
168                     contentLength = Integer.parseInt(value);
169                 } catch (NumberFormatException ignored) {
170                 }
171             } else if ("Connection".equalsIgnoreCase(fieldName)) {
172                 connection = value;
173             } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
174                 sentRequestMillis = Long.parseLong(value);
175             } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
176                 receivedResponseMillis = Long.parseLong(value);
177             }
178         }
179     }
180 
isContentEncodingGzip()181     public boolean isContentEncodingGzip() {
182         return "gzip".equalsIgnoreCase(contentEncoding);
183     }
184 
stripContentEncoding()185     public void stripContentEncoding() {
186         contentEncoding = null;
187         headers.removeAll("Content-Encoding");
188     }
189 
isChunked()190     public boolean isChunked() {
191         return "chunked".equalsIgnoreCase(transferEncoding);
192     }
193 
hasConnectionClose()194     public boolean hasConnectionClose() {
195         return "close".equalsIgnoreCase(connection);
196     }
197 
getUri()198     public URI getUri() {
199         return uri;
200     }
201 
getHeaders()202     public RawHeaders getHeaders() {
203         return headers;
204     }
205 
getServedDate()206     public Date getServedDate() {
207         return servedDate;
208     }
209 
getLastModified()210     public Date getLastModified() {
211         return lastModified;
212     }
213 
getExpires()214     public Date getExpires() {
215         return expires;
216     }
217 
isNoCache()218     public boolean isNoCache() {
219         return noCache;
220     }
221 
isNoStore()222     public boolean isNoStore() {
223         return noStore;
224     }
225 
getMaxAgeSeconds()226     public int getMaxAgeSeconds() {
227         return maxAgeSeconds;
228     }
229 
getSMaxAgeSeconds()230     public int getSMaxAgeSeconds() {
231         return sMaxAgeSeconds;
232     }
233 
isPublic()234     public boolean isPublic() {
235         return isPublic;
236     }
237 
isMustRevalidate()238     public boolean isMustRevalidate() {
239         return mustRevalidate;
240     }
241 
getEtag()242     public String getEtag() {
243         return etag;
244     }
245 
getVaryFields()246     public Set<String> getVaryFields() {
247         return varyFields;
248     }
249 
getContentEncoding()250     public String getContentEncoding() {
251         return contentEncoding;
252     }
253 
getContentLength()254     public int getContentLength() {
255         return contentLength;
256     }
257 
getConnection()258     public String getConnection() {
259         return connection;
260     }
261 
setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis)262     public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
263         this.sentRequestMillis = sentRequestMillis;
264         headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
265         this.receivedResponseMillis = receivedResponseMillis;
266         headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
267     }
268 
269     /**
270      * Returns the current age of the response, in milliseconds. The calculation
271      * is specified by RFC 2616, 13.2.3 Age Calculations.
272      */
computeAge(long nowMillis)273     private long computeAge(long nowMillis) {
274         long apparentReceivedAge = servedDate != null
275                 ? Math.max(0, receivedResponseMillis - servedDate.getTime())
276                 : 0;
277         long receivedAge = ageSeconds != -1
278                 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
279                 : apparentReceivedAge;
280         long responseDuration = receivedResponseMillis - sentRequestMillis;
281         long residentDuration = nowMillis - receivedResponseMillis;
282         return receivedAge + responseDuration + residentDuration;
283     }
284 
285     /**
286      * Returns the number of milliseconds that the response was fresh for,
287      * starting from the served date.
288      */
computeFreshnessLifetime()289     private long computeFreshnessLifetime() {
290         if (maxAgeSeconds != -1) {
291             return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
292         } else if (expires != null) {
293             long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
294             long delta = expires.getTime() - servedMillis;
295             return delta > 0 ? delta : 0;
296         } else if (lastModified != null && uri.getRawQuery() == null) {
297             /*
298              * As recommended by the HTTP RFC and implemented in Firefox, the
299              * max age of a document should be defaulted to 10% of the
300              * document's age at the time it was served. Default expiration
301              * dates aren't used for URIs containing a query.
302              */
303             long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
304             long delta = servedMillis - lastModified.getTime();
305             return delta > 0 ? (delta / 10) : 0;
306         }
307         return 0;
308     }
309 
310     /**
311      * Returns true if computeFreshnessLifetime used a heuristic. If we used a
312      * heuristic to serve a cached response older than 24 hours, we are required
313      * to attach a warning.
314      */
isFreshnessLifetimeHeuristic()315     private boolean isFreshnessLifetimeHeuristic() {
316         return maxAgeSeconds == -1 && expires == null;
317     }
318 
319     /**
320      * Returns true if this response can be stored to later serve another
321      * request.
322      */
isCacheable(RequestHeaders request)323     public boolean isCacheable(RequestHeaders request) {
324         /*
325          * Always go to network for uncacheable response codes (RFC 2616, 13.4),
326          * This implementation doesn't support caching partial content.
327          */
328         int responseCode = headers.getResponseCode();
329         if (responseCode != HttpURLConnection.HTTP_OK
330                 && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
331                 && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
332                 && responseCode != HttpURLConnection.HTTP_MOVED_PERM
333                 && responseCode != HttpURLConnection.HTTP_GONE) {
334             return false;
335         }
336 
337         /*
338          * Responses to authorized requests aren't cacheable unless they include
339          * a 'public', 'must-revalidate' or 's-maxage' directive.
340          */
341         if (request.hasAuthorization()
342                 && !isPublic
343                 && !mustRevalidate
344                 && sMaxAgeSeconds == -1) {
345             return false;
346         }
347 
348         if (noStore) {
349             return false;
350         }
351 
352         return true;
353     }
354 
355     /**
356      * Returns true if a Vary header contains an asterisk. Such responses cannot
357      * be cached.
358      */
hasVaryAll()359     public boolean hasVaryAll() {
360         return varyFields.contains("*");
361     }
362 
363     /**
364      * Returns true if none of the Vary headers on this response have changed
365      * between {@code cachedRequest} and {@code newRequest}.
366      */
varyMatches(Map<String, List<String>> cachedRequest, Map<String, List<String>> newRequest)367     public boolean varyMatches(Map<String, List<String>> cachedRequest,
368             Map<String, List<String>> newRequest) {
369         for (String field : varyFields) {
370             if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) {
371                 return false;
372             }
373         }
374         return true;
375     }
376 
377     /**
378      * Returns the source to satisfy {@code request} given this cached response.
379      */
chooseResponseSource(long nowMillis, RequestHeaders request)380     public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
381         /*
382          * If this response shouldn't have been stored, it should never be used
383          * as a response source. This check should be redundant as long as the
384          * persistence store is well-behaved and the rules are constant.
385          */
386         if (!isCacheable(request)) {
387             return ResponseSource.NETWORK;
388         }
389 
390         if (request.isNoCache() || request.hasConditions()) {
391             return ResponseSource.NETWORK;
392         }
393 
394         long ageMillis = computeAge(nowMillis);
395         long freshMillis = computeFreshnessLifetime();
396 
397         if (request.getMaxAgeSeconds() != -1) {
398             freshMillis = Math.min(freshMillis,
399                     TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
400         }
401 
402         long minFreshMillis = 0;
403         if (request.getMinFreshSeconds() != -1) {
404             minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
405         }
406 
407         long maxStaleMillis = 0;
408         if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
409             maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
410         }
411 
412         if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
413             if (ageMillis + minFreshMillis >= freshMillis) {
414                 headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
415             }
416             if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
417                 headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
418             }
419             return ResponseSource.CACHE;
420         }
421 
422         if (lastModified != null) {
423             request.setIfModifiedSince(lastModified);
424         } else if (servedDate != null) {
425             request.setIfModifiedSince(servedDate);
426         }
427 
428         if (etag != null) {
429             request.setIfNoneMatch(etag);
430         }
431 
432         return request.hasConditions()
433                 ? ResponseSource.CONDITIONAL_CACHE
434                 : ResponseSource.NETWORK;
435     }
436 
437     /**
438      * Returns true if this cached response should be used; false if the
439      * network response should be used.
440      */
validate(ResponseHeaders networkResponse)441     public boolean validate(ResponseHeaders networkResponse) {
442         if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
443             return true;
444         }
445 
446         /*
447          * The HTTP spec says that if the network's response is older than our
448          * cached response, we may return the cache's response. Like Chrome (but
449          * unlike Firefox), this client prefers to return the newer response.
450          */
451         if (lastModified != null
452                 && networkResponse.lastModified != null
453                 && networkResponse.lastModified.getTime() < lastModified.getTime()) {
454             return true;
455         }
456 
457         return false;
458     }
459 
460     /**
461      * Combines this cached header with a network header as defined by RFC 2616,
462      * 13.5.3.
463      */
combine(ResponseHeaders network)464     public ResponseHeaders combine(ResponseHeaders network) {
465         RawHeaders result = new RawHeaders();
466         result.setStatusLine(headers.getStatusLine());
467 
468         for (int i = 0; i < headers.length(); i++) {
469             String fieldName = headers.getFieldName(i);
470             String value = headers.getValue(i);
471             if (fieldName.equals("Warning") && value.startsWith("1")) {
472                 continue; // drop 100-level freshness warnings
473             }
474             if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
475                 result.add(fieldName, value);
476             }
477         }
478 
479         for (int i = 0; i < network.headers.length(); i++) {
480             String fieldName = network.headers.getFieldName(i);
481             if (isEndToEnd(fieldName)) {
482                 result.add(fieldName, network.headers.getValue(i));
483             }
484         }
485 
486         return new ResponseHeaders(uri, result);
487     }
488 
489     /**
490      * Returns true if {@code fieldName} is an end-to-end HTTP header, as
491      * defined by RFC 2616, 13.5.1.
492      */
isEndToEnd(String fieldName)493     private static boolean isEndToEnd(String fieldName) {
494         return !fieldName.equalsIgnoreCase("Connection")
495                 && !fieldName.equalsIgnoreCase("Keep-Alive")
496                 && !fieldName.equalsIgnoreCase("Proxy-Authenticate")
497                 && !fieldName.equalsIgnoreCase("Proxy-Authorization")
498                 && !fieldName.equalsIgnoreCase("TE")
499                 && !fieldName.equalsIgnoreCase("Trailers")
500                 && !fieldName.equalsIgnoreCase("Transfer-Encoding")
501                 && !fieldName.equalsIgnoreCase("Upgrade");
502     }
503 }
504