| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.squareup.okhttp.internal.http; |
| |
| import com.squareup.okhttp.ResponseSource; |
| import com.squareup.okhttp.internal.Platform; |
| import java.io.IOException; |
| import java.net.HttpURLConnection; |
| import java.net.URI; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.TimeUnit; |
| |
| import static com.squareup.okhttp.internal.Util.equal; |
| |
| /** Parsed HTTP response headers. */ |
| public final class ResponseHeaders { |
| |
| /** HTTP header name for the local time when the request was sent. */ |
| private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis"; |
| |
| /** HTTP header name for the local time when the response was received. */ |
| private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis"; |
| |
| /** HTTP synthetic header with the response source. */ |
| static final String RESPONSE_SOURCE = Platform.get().getPrefix() + "-Response-Source"; |
| |
| /** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc). */ |
| static final String SELECTED_TRANSPORT = Platform.get().getPrefix() + "-Selected-Transport"; |
| |
| private final URI uri; |
| private final RawHeaders headers; |
| |
| /** The server's time when this response was served, if known. */ |
| private Date servedDate; |
| |
| /** The last modified date of the response, if known. */ |
| private Date lastModified; |
| |
| /** |
| * The expiration date of the response, if known. If both this field and the |
| * max age are set, the max age is preferred. |
| */ |
| private Date expires; |
| |
| /** |
| * Extension header set by HttpURLConnectionImpl specifying the timestamp |
| * when the HTTP request was first initiated. |
| */ |
| private long sentRequestMillis; |
| |
| /** |
| * Extension header set by HttpURLConnectionImpl specifying the timestamp |
| * when the HTTP response was first received. |
| */ |
| private long receivedResponseMillis; |
| |
| /** |
| * In the response, this field's name "no-cache" is misleading. It doesn't |
| * prevent us from caching the response; it only means we have to validate |
| * the response with the origin server before returning it. We can do this |
| * with a conditional get. |
| */ |
| private boolean noCache; |
| |
| /** If true, this response should not be cached. */ |
| private boolean noStore; |
| |
| /** |
| * The duration past the response's served date that it can be served |
| * without validation. |
| */ |
| private int maxAgeSeconds = -1; |
| |
| /** |
| * The "s-maxage" directive is the max age for shared caches. Not to be |
| * confused with "max-age" for non-shared caches, As in Firefox and Chrome, |
| * this directive is not honored by this cache. |
| */ |
| private int sMaxAgeSeconds = -1; |
| |
| /** |
| * This request header field's name "only-if-cached" is misleading. It |
| * actually means "do not use the network". It is set by a client who only |
| * wants to make a request if it can be fully satisfied by the cache. |
| * Cached responses that would require validation (ie. conditional gets) are |
| * not permitted if this header is set. |
| */ |
| private boolean isPublic; |
| private boolean mustRevalidate; |
| private String etag; |
| private int ageSeconds = -1; |
| |
| /** Case-insensitive set of field names. */ |
| private Set<String> varyFields = Collections.emptySet(); |
| |
| private String contentEncoding; |
| private String transferEncoding; |
| private int contentLength = -1; |
| private String connection; |
| |
| public ResponseHeaders(URI uri, RawHeaders headers) { |
| this.uri = uri; |
| this.headers = headers; |
| |
| HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { |
| @Override public void handle(String directive, String parameter) { |
| if ("no-cache".equalsIgnoreCase(directive)) { |
| noCache = true; |
| } else if ("no-store".equalsIgnoreCase(directive)) { |
| noStore = true; |
| } else if ("max-age".equalsIgnoreCase(directive)) { |
| maxAgeSeconds = HeaderParser.parseSeconds(parameter); |
| } else if ("s-maxage".equalsIgnoreCase(directive)) { |
| sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); |
| } else if ("public".equalsIgnoreCase(directive)) { |
| isPublic = true; |
| } else if ("must-revalidate".equalsIgnoreCase(directive)) { |
| mustRevalidate = true; |
| } |
| } |
| }; |
| |
| for (int i = 0; i < headers.length(); i++) { |
| String fieldName = headers.getFieldName(i); |
| String value = headers.getValue(i); |
| if ("Cache-Control".equalsIgnoreCase(fieldName)) { |
| HeaderParser.parseCacheControl(value, handler); |
| } else if ("Date".equalsIgnoreCase(fieldName)) { |
| servedDate = HttpDate.parse(value); |
| } else if ("Expires".equalsIgnoreCase(fieldName)) { |
| expires = HttpDate.parse(value); |
| } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { |
| lastModified = HttpDate.parse(value); |
| } else if ("ETag".equalsIgnoreCase(fieldName)) { |
| etag = value; |
| } else if ("Pragma".equalsIgnoreCase(fieldName)) { |
| if ("no-cache".equalsIgnoreCase(value)) { |
| noCache = true; |
| } |
| } else if ("Age".equalsIgnoreCase(fieldName)) { |
| ageSeconds = HeaderParser.parseSeconds(value); |
| } else if ("Vary".equalsIgnoreCase(fieldName)) { |
| // Replace the immutable empty set with something we can mutate. |
| if (varyFields.isEmpty()) { |
| varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); |
| } |
| for (String varyField : value.split(",")) { |
| varyFields.add(varyField.trim()); |
| } |
| } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { |
| contentEncoding = value; |
| } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { |
| transferEncoding = value; |
| } else if ("Content-Length".equalsIgnoreCase(fieldName)) { |
| try { |
| contentLength = Integer.parseInt(value); |
| } catch (NumberFormatException ignored) { |
| } |
| } else if ("Connection".equalsIgnoreCase(fieldName)) { |
| connection = value; |
| } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) { |
| sentRequestMillis = Long.parseLong(value); |
| } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { |
| receivedResponseMillis = Long.parseLong(value); |
| } |
| } |
| } |
| |
| public boolean isContentEncodingGzip() { |
| return "gzip".equalsIgnoreCase(contentEncoding); |
| } |
| |
| public void stripContentEncoding() { |
| contentEncoding = null; |
| headers.removeAll("Content-Encoding"); |
| } |
| |
| public void stripContentLength() { |
| contentLength = -1; |
| headers.removeAll("Content-Length"); |
| } |
| |
| public boolean isChunked() { |
| return "chunked".equalsIgnoreCase(transferEncoding); |
| } |
| |
| public boolean hasConnectionClose() { |
| return "close".equalsIgnoreCase(connection); |
| } |
| |
| public URI getUri() { |
| return uri; |
| } |
| |
| public RawHeaders getHeaders() { |
| return headers; |
| } |
| |
| public Date getServedDate() { |
| return servedDate; |
| } |
| |
| public Date getLastModified() { |
| return lastModified; |
| } |
| |
| public Date getExpires() { |
| return expires; |
| } |
| |
| public boolean isNoCache() { |
| return noCache; |
| } |
| |
| public boolean isNoStore() { |
| return noStore; |
| } |
| |
| public int getMaxAgeSeconds() { |
| return maxAgeSeconds; |
| } |
| |
| public int getSMaxAgeSeconds() { |
| return sMaxAgeSeconds; |
| } |
| |
| public boolean isPublic() { |
| return isPublic; |
| } |
| |
| public boolean isMustRevalidate() { |
| return mustRevalidate; |
| } |
| |
| public String getEtag() { |
| return etag; |
| } |
| |
| public Set<String> getVaryFields() { |
| return varyFields; |
| } |
| |
| public String getContentEncoding() { |
| return contentEncoding; |
| } |
| |
| public int getContentLength() { |
| return contentLength; |
| } |
| |
| public String getConnection() { |
| return connection; |
| } |
| |
| public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) { |
| this.sentRequestMillis = sentRequestMillis; |
| headers.add(SENT_MILLIS, Long.toString(sentRequestMillis)); |
| this.receivedResponseMillis = receivedResponseMillis; |
| headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); |
| } |
| |
| public void setResponseSource(ResponseSource responseSource) { |
| headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode()); |
| } |
| |
| public void setTransport(String transport) { |
| headers.set(SELECTED_TRANSPORT, transport); |
| } |
| |
| /** |
| * Returns the current age of the response, in milliseconds. The calculation |
| * is specified by RFC 2616, 13.2.3 Age Calculations. |
| */ |
| private long computeAge(long nowMillis) { |
| long apparentReceivedAge = |
| servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0; |
| long receivedAge = |
| ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) |
| : apparentReceivedAge; |
| long responseDuration = receivedResponseMillis - sentRequestMillis; |
| long residentDuration = nowMillis - receivedResponseMillis; |
| return receivedAge + responseDuration + residentDuration; |
| } |
| |
| /** |
| * Returns the number of milliseconds that the response was fresh for, |
| * starting from the served date. |
| */ |
| private long computeFreshnessLifetime() { |
| if (maxAgeSeconds != -1) { |
| return TimeUnit.SECONDS.toMillis(maxAgeSeconds); |
| } else if (expires != null) { |
| long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; |
| long delta = expires.getTime() - servedMillis; |
| return delta > 0 ? delta : 0; |
| } else if (lastModified != null && uri.getRawQuery() == null) { |
| // As recommended by the HTTP RFC and implemented in Firefox, the |
| // max age of a document should be defaulted to 10% of the |
| // document's age at the time it was served. Default expiration |
| // dates aren't used for URIs containing a query. |
| long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; |
| long delta = servedMillis - lastModified.getTime(); |
| return delta > 0 ? (delta / 10) : 0; |
| } |
| return 0; |
| } |
| |
| /** |
| * Returns true if computeFreshnessLifetime used a heuristic. If we used a |
| * heuristic to serve a cached response older than 24 hours, we are required |
| * to attach a warning. |
| */ |
| private boolean isFreshnessLifetimeHeuristic() { |
| return maxAgeSeconds == -1 && expires == null; |
| } |
| |
| /** |
| * Returns true if this response can be stored to later serve another |
| * request. |
| */ |
| public boolean isCacheable(RequestHeaders request) { |
| // Always go to network for uncacheable response codes (RFC 2616, 13.4), |
| // This implementation doesn't support caching partial content. |
| int responseCode = headers.getResponseCode(); |
| if (responseCode != HttpURLConnection.HTTP_OK |
| && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE |
| && responseCode != HttpURLConnection.HTTP_MULT_CHOICE |
| && responseCode != HttpURLConnection.HTTP_MOVED_PERM |
| && responseCode != HttpURLConnection.HTTP_GONE) { |
| return false; |
| } |
| |
| // Responses to authorized requests aren't cacheable unless they include |
| // a 'public', 'must-revalidate' or 's-maxage' directive. |
| if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) { |
| return false; |
| } |
| |
| if (noStore) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Returns true if a Vary header contains an asterisk. Such responses cannot |
| * be cached. |
| */ |
| public boolean hasVaryAll() { |
| return varyFields.contains("*"); |
| } |
| |
| /** |
| * Returns true if none of the Vary headers on this response have changed |
| * between {@code cachedRequest} and {@code newRequest}. |
| */ |
| public boolean varyMatches(Map<String, List<String>> cachedRequest, |
| Map<String, List<String>> newRequest) { |
| for (String field : varyFields) { |
| if (!equal(cachedRequest.get(field), newRequest.get(field))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** Returns the source to satisfy {@code request} given this cached response. */ |
| public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) { |
| // If this response shouldn't have been stored, it should never be used |
| // as a response source. This check should be redundant as long as the |
| // persistence store is well-behaved and the rules are constant. |
| if (!isCacheable(request)) { |
| return ResponseSource.NETWORK; |
| } |
| |
| if (request.isNoCache() || request.hasConditions()) { |
| return ResponseSource.NETWORK; |
| } |
| |
| long ageMillis = computeAge(nowMillis); |
| long freshMillis = computeFreshnessLifetime(); |
| |
| if (request.getMaxAgeSeconds() != -1) { |
| freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); |
| } |
| |
| long minFreshMillis = 0; |
| if (request.getMinFreshSeconds() != -1) { |
| minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); |
| } |
| |
| long maxStaleMillis = 0; |
| if (!mustRevalidate && request.getMaxStaleSeconds() != -1) { |
| maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); |
| } |
| |
| if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { |
| if (ageMillis + minFreshMillis >= freshMillis) { |
| headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); |
| } |
| long oneDayMillis = 24 * 60 * 60 * 1000L; |
| if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { |
| headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); |
| } |
| return ResponseSource.CACHE; |
| } |
| |
| if (lastModified != null) { |
| request.setIfModifiedSince(lastModified); |
| } else if (servedDate != null) { |
| request.setIfModifiedSince(servedDate); |
| } |
| |
| if (etag != null) { |
| request.setIfNoneMatch(etag); |
| } |
| |
| return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK; |
| } |
| |
| /** |
| * Returns true if this cached response should be used; false if the |
| * network response should be used. |
| */ |
| public boolean validate(ResponseHeaders networkResponse) { |
| if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { |
| return true; |
| } |
| |
| // The HTTP spec says that if the network's response is older than our |
| // cached response, we may return the cache's response. Like Chrome (but |
| // unlike Firefox), this client prefers to return the newer response. |
| if (lastModified != null |
| && networkResponse.lastModified != null |
| && networkResponse.lastModified.getTime() < lastModified.getTime()) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Combines this cached header with a network header as defined by RFC 2616, |
| * 13.5.3. |
| */ |
| public ResponseHeaders combine(ResponseHeaders network) throws IOException { |
| RawHeaders result = new RawHeaders(); |
| result.setStatusLine(headers.getStatusLine()); |
| |
| for (int i = 0; i < headers.length(); i++) { |
| String fieldName = headers.getFieldName(i); |
| String value = headers.getValue(i); |
| if ("Warning".equals(fieldName) && value.startsWith("1")) { |
| continue; // drop 100-level freshness warnings |
| } |
| if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) { |
| result.add(fieldName, value); |
| } |
| } |
| |
| for (int i = 0; i < network.headers.length(); i++) { |
| String fieldName = network.headers.getFieldName(i); |
| if (isEndToEnd(fieldName)) { |
| result.add(fieldName, network.headers.getValue(i)); |
| } |
| } |
| |
| return new ResponseHeaders(uri, result); |
| } |
| |
| /** |
| * Returns true if {@code fieldName} is an end-to-end HTTP header, as |
| * defined by RFC 2616, 13.5.1. |
| */ |
| private static boolean isEndToEnd(String fieldName) { |
| return !"Connection".equalsIgnoreCase(fieldName) |
| && !"Keep-Alive".equalsIgnoreCase(fieldName) |
| && !"Proxy-Authenticate".equalsIgnoreCase(fieldName) |
| && !"Proxy-Authorization".equalsIgnoreCase(fieldName) |
| && !"TE".equalsIgnoreCase(fieldName) |
| && !"Trailers".equalsIgnoreCase(fieldName) |
| && !"Transfer-Encoding".equalsIgnoreCase(fieldName) |
| && !"Upgrade".equalsIgnoreCase(fieldName); |
| } |
| } |