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