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