Home | History | Annotate | Download | only in http
      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 
    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 
    181     public boolean isContentEncodingGzip() {
    182         return "gzip".equalsIgnoreCase(contentEncoding);
    183     }
    184 
    185     public void stripContentEncoding() {
    186         contentEncoding = null;
    187         headers.removeAll("Content-Encoding");
    188     }
    189 
    190     public void stripContentLength() {
    191         contentLength = -1;
    192         headers.removeAll("Content-Length");
    193     }
    194 
    195     public boolean isChunked() {
    196         return "chunked".equalsIgnoreCase(transferEncoding);
    197     }
    198 
    199     public boolean hasConnectionClose() {
    200         return "close".equalsIgnoreCase(connection);
    201     }
    202 
    203     public URI getUri() {
    204         return uri;
    205     }
    206 
    207     public RawHeaders getHeaders() {
    208         return headers;
    209     }
    210 
    211     public Date getServedDate() {
    212         return servedDate;
    213     }
    214 
    215     public Date getLastModified() {
    216         return lastModified;
    217     }
    218 
    219     public Date getExpires() {
    220         return expires;
    221     }
    222 
    223     public boolean isNoCache() {
    224         return noCache;
    225     }
    226 
    227     public boolean isNoStore() {
    228         return noStore;
    229     }
    230 
    231     public int getMaxAgeSeconds() {
    232         return maxAgeSeconds;
    233     }
    234 
    235     public int getSMaxAgeSeconds() {
    236         return sMaxAgeSeconds;
    237     }
    238 
    239     public boolean isPublic() {
    240         return isPublic;
    241     }
    242 
    243     public boolean isMustRevalidate() {
    244         return mustRevalidate;
    245     }
    246 
    247     public String getEtag() {
    248         return etag;
    249     }
    250 
    251     public Set<String> getVaryFields() {
    252         return varyFields;
    253     }
    254 
    255     public String getContentEncoding() {
    256         return contentEncoding;
    257     }
    258 
    259     public int getContentLength() {
    260         return contentLength;
    261     }
    262 
    263     public String getConnection() {
    264         return connection;
    265     }
    266 
    267     public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
    268         this.sentRequestMillis = sentRequestMillis;
    269         headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
    270         this.receivedResponseMillis = receivedResponseMillis;
    271         headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
    272     }
    273 
    274     /**
    275      * Returns the current age of the response, in milliseconds. The calculation
    276      * is specified by RFC 2616, 13.2.3 Age Calculations.
    277      */
    278     private long computeAge(long nowMillis) {
    279         long apparentReceivedAge = servedDate != null
    280                 ? Math.max(0, receivedResponseMillis - servedDate.getTime())
    281                 : 0;
    282         long receivedAge = ageSeconds != -1
    283                 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
    284                 : apparentReceivedAge;
    285         long responseDuration = receivedResponseMillis - sentRequestMillis;
    286         long residentDuration = nowMillis - receivedResponseMillis;
    287         return receivedAge + responseDuration + residentDuration;
    288     }
    289 
    290     /**
    291      * Returns the number of milliseconds that the response was fresh for,
    292      * starting from the served date.
    293      */
    294     private long computeFreshnessLifetime() {
    295         if (maxAgeSeconds != -1) {
    296             return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
    297         } else if (expires != null) {
    298             long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
    299             long delta = expires.getTime() - servedMillis;
    300             return delta > 0 ? delta : 0;
    301         } else if (lastModified != null && uri.getRawQuery() == null) {
    302             /*
    303              * As recommended by the HTTP RFC and implemented in Firefox, the
    304              * max age of a document should be defaulted to 10% of the
    305              * document's age at the time it was served. Default expiration
    306              * dates aren't used for URIs containing a query.
    307              */
    308             long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
    309             long delta = servedMillis - lastModified.getTime();
    310             return delta > 0 ? (delta / 10) : 0;
    311         }
    312         return 0;
    313     }
    314 
    315     /**
    316      * Returns true if computeFreshnessLifetime used a heuristic. If we used a
    317      * heuristic to serve a cached response older than 24 hours, we are required
    318      * to attach a warning.
    319      */
    320     private boolean isFreshnessLifetimeHeuristic() {
    321         return maxAgeSeconds == -1 && expires == null;
    322     }
    323 
    324     /**
    325      * Returns true if this response can be stored to later serve another
    326      * request.
    327      */
    328     public boolean isCacheable(RequestHeaders request) {
    329         /*
    330          * Always go to network for uncacheable response codes (RFC 2616, 13.4),
    331          * This implementation doesn't support caching partial content.
    332          */
    333         int responseCode = headers.getResponseCode();
    334         if (responseCode != HttpURLConnection.HTTP_OK
    335                 && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
    336                 && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
    337                 && responseCode != HttpURLConnection.HTTP_MOVED_PERM
    338                 && responseCode != HttpURLConnection.HTTP_GONE) {
    339             return false;
    340         }
    341 
    342         /*
    343          * Responses to authorized requests aren't cacheable unless they include
    344          * a 'public', 'must-revalidate' or 's-maxage' directive.
    345          */
    346         if (request.hasAuthorization()
    347                 && !isPublic
    348                 && !mustRevalidate
    349                 && sMaxAgeSeconds == -1) {
    350             return false;
    351         }
    352 
    353         if (noStore) {
    354             return false;
    355         }
    356 
    357         return true;
    358     }
    359 
    360     /**
    361      * Returns true if a Vary header contains an asterisk. Such responses cannot
    362      * be cached.
    363      */
    364     public boolean hasVaryAll() {
    365         return varyFields.contains("*");
    366     }
    367 
    368     /**
    369      * Returns true if none of the Vary headers on this response have changed
    370      * between {@code cachedRequest} and {@code newRequest}.
    371      */
    372     public boolean varyMatches(Map<String, List<String>> cachedRequest,
    373             Map<String, List<String>> newRequest) {
    374         for (String field : varyFields) {
    375             if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) {
    376                 return false;
    377             }
    378         }
    379         return true;
    380     }
    381 
    382     /**
    383      * Returns the source to satisfy {@code request} given this cached response.
    384      */
    385     public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
    386         /*
    387          * If this response shouldn't have been stored, it should never be used
    388          * as a response source. This check should be redundant as long as the
    389          * persistence store is well-behaved and the rules are constant.
    390          */
    391         if (!isCacheable(request)) {
    392             return ResponseSource.NETWORK;
    393         }
    394 
    395         if (request.isNoCache() || request.hasConditions()) {
    396             return ResponseSource.NETWORK;
    397         }
    398 
    399         long ageMillis = computeAge(nowMillis);
    400         long freshMillis = computeFreshnessLifetime();
    401 
    402         if (request.getMaxAgeSeconds() != -1) {
    403             freshMillis = Math.min(freshMillis,
    404                     TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
    405         }
    406 
    407         long minFreshMillis = 0;
    408         if (request.getMinFreshSeconds() != -1) {
    409             minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
    410         }
    411 
    412         long maxStaleMillis = 0;
    413         if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
    414             maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
    415         }
    416 
    417         if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    418             if (ageMillis + minFreshMillis >= freshMillis) {
    419                 headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
    420             }
    421             if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
    422                 headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    423             }
    424             return ResponseSource.CACHE;
    425         }
    426 
    427         if (lastModified != null) {
    428             request.setIfModifiedSince(lastModified);
    429         } else if (servedDate != null) {
    430             request.setIfModifiedSince(servedDate);
    431         }
    432 
    433         if (etag != null) {
    434             request.setIfNoneMatch(etag);
    435         }
    436 
    437         return request.hasConditions()
    438                 ? ResponseSource.CONDITIONAL_CACHE
    439                 : ResponseSource.NETWORK;
    440     }
    441 
    442     /**
    443      * Returns true if this cached response should be used; false if the
    444      * network response should be used.
    445      */
    446     public boolean validate(ResponseHeaders networkResponse) {
    447         if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
    448             return true;
    449         }
    450 
    451         /*
    452          * The HTTP spec says that if the network's response is older than our
    453          * cached response, we may return the cache's response. Like Chrome (but
    454          * unlike Firefox), this client prefers to return the newer response.
    455          */
    456         if (lastModified != null
    457                 && networkResponse.lastModified != null
    458                 && networkResponse.lastModified.getTime() < lastModified.getTime()) {
    459             return true;
    460         }
    461 
    462         return false;
    463     }
    464 
    465     /**
    466      * Combines this cached header with a network header as defined by RFC 2616,
    467      * 13.5.3.
    468      */
    469     public ResponseHeaders combine(ResponseHeaders network) {
    470         RawHeaders result = new RawHeaders();
    471         result.setStatusLine(headers.getStatusLine());
    472 
    473         for (int i = 0; i < headers.length(); i++) {
    474             String fieldName = headers.getFieldName(i);
    475             String value = headers.getValue(i);
    476             if (fieldName.equals("Warning") && value.startsWith("1")) {
    477                 continue; // drop 100-level freshness warnings
    478             }
    479             if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
    480                 result.add(fieldName, value);
    481             }
    482         }
    483 
    484         for (int i = 0; i < network.headers.length(); i++) {
    485             String fieldName = network.headers.getFieldName(i);
    486             if (isEndToEnd(fieldName)) {
    487                 result.add(fieldName, network.headers.getValue(i));
    488             }
    489         }
    490 
    491         return new ResponseHeaders(uri, result);
    492     }
    493 
    494     /**
    495      * Returns true if {@code fieldName} is an end-to-end HTTP header, as
    496      * defined by RFC 2616, 13.5.1.
    497      */
    498     private static boolean isEndToEnd(String fieldName) {
    499         return !fieldName.equalsIgnoreCase("Connection")
    500                 && !fieldName.equalsIgnoreCase("Keep-Alive")
    501                 && !fieldName.equalsIgnoreCase("Proxy-Authenticate")
    502                 && !fieldName.equalsIgnoreCase("Proxy-Authorization")
    503                 && !fieldName.equalsIgnoreCase("TE")
    504                 && !fieldName.equalsIgnoreCase("Trailers")
    505                 && !fieldName.equalsIgnoreCase("Transfer-Encoding")
    506                 && !fieldName.equalsIgnoreCase("Upgrade");
    507     }
    508 }
    509