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