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