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