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
17package com.squareup.okhttp.internal.http;
18
19import com.squareup.okhttp.ResponseSource;
20import com.squareup.okhttp.internal.Platform;
21import java.io.IOException;
22import java.net.HttpURLConnection;
23import java.net.URI;
24import java.util.Collections;
25import java.util.Date;
26import java.util.List;
27import java.util.Map;
28import java.util.Set;
29import java.util.TreeSet;
30import java.util.concurrent.TimeUnit;
31
32import static com.squareup.okhttp.internal.Util.equal;
33
34/** Parsed HTTP response headers. */
35public 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