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 ResponseHeaders(URI uri, RawHeaders headers)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 isContentEncodingGzip()181 public boolean isContentEncodingGzip() { 182 return "gzip".equalsIgnoreCase(contentEncoding); 183 } 184 stripContentEncoding()185 public void stripContentEncoding() { 186 contentEncoding = null; 187 headers.removeAll("Content-Encoding"); 188 } 189 isChunked()190 public boolean isChunked() { 191 return "chunked".equalsIgnoreCase(transferEncoding); 192 } 193 hasConnectionClose()194 public boolean hasConnectionClose() { 195 return "close".equalsIgnoreCase(connection); 196 } 197 getUri()198 public URI getUri() { 199 return uri; 200 } 201 getHeaders()202 public RawHeaders getHeaders() { 203 return headers; 204 } 205 getServedDate()206 public Date getServedDate() { 207 return servedDate; 208 } 209 getLastModified()210 public Date getLastModified() { 211 return lastModified; 212 } 213 getExpires()214 public Date getExpires() { 215 return expires; 216 } 217 isNoCache()218 public boolean isNoCache() { 219 return noCache; 220 } 221 isNoStore()222 public boolean isNoStore() { 223 return noStore; 224 } 225 getMaxAgeSeconds()226 public int getMaxAgeSeconds() { 227 return maxAgeSeconds; 228 } 229 getSMaxAgeSeconds()230 public int getSMaxAgeSeconds() { 231 return sMaxAgeSeconds; 232 } 233 isPublic()234 public boolean isPublic() { 235 return isPublic; 236 } 237 isMustRevalidate()238 public boolean isMustRevalidate() { 239 return mustRevalidate; 240 } 241 getEtag()242 public String getEtag() { 243 return etag; 244 } 245 getVaryFields()246 public Set<String> getVaryFields() { 247 return varyFields; 248 } 249 getContentEncoding()250 public String getContentEncoding() { 251 return contentEncoding; 252 } 253 getContentLength()254 public int getContentLength() { 255 return contentLength; 256 } 257 getConnection()258 public String getConnection() { 259 return connection; 260 } 261 setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis)262 public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) { 263 this.sentRequestMillis = sentRequestMillis; 264 headers.add(SENT_MILLIS, Long.toString(sentRequestMillis)); 265 this.receivedResponseMillis = receivedResponseMillis; 266 headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); 267 } 268 269 /** 270 * Returns the current age of the response, in milliseconds. The calculation 271 * is specified by RFC 2616, 13.2.3 Age Calculations. 272 */ computeAge(long nowMillis)273 private long computeAge(long nowMillis) { 274 long apparentReceivedAge = servedDate != null 275 ? Math.max(0, receivedResponseMillis - servedDate.getTime()) 276 : 0; 277 long receivedAge = ageSeconds != -1 278 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) 279 : apparentReceivedAge; 280 long responseDuration = receivedResponseMillis - sentRequestMillis; 281 long residentDuration = nowMillis - receivedResponseMillis; 282 return receivedAge + responseDuration + residentDuration; 283 } 284 285 /** 286 * Returns the number of milliseconds that the response was fresh for, 287 * starting from the served date. 288 */ computeFreshnessLifetime()289 private long computeFreshnessLifetime() { 290 if (maxAgeSeconds != -1) { 291 return TimeUnit.SECONDS.toMillis(maxAgeSeconds); 292 } else if (expires != null) { 293 long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; 294 long delta = expires.getTime() - servedMillis; 295 return delta > 0 ? delta : 0; 296 } else if (lastModified != null && uri.getRawQuery() == null) { 297 /* 298 * As recommended by the HTTP RFC and implemented in Firefox, the 299 * max age of a document should be defaulted to 10% of the 300 * document's age at the time it was served. Default expiration 301 * dates aren't used for URIs containing a query. 302 */ 303 long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; 304 long delta = servedMillis - lastModified.getTime(); 305 return delta > 0 ? (delta / 10) : 0; 306 } 307 return 0; 308 } 309 310 /** 311 * Returns true if computeFreshnessLifetime used a heuristic. If we used a 312 * heuristic to serve a cached response older than 24 hours, we are required 313 * to attach a warning. 314 */ isFreshnessLifetimeHeuristic()315 private boolean isFreshnessLifetimeHeuristic() { 316 return maxAgeSeconds == -1 && expires == null; 317 } 318 319 /** 320 * Returns true if this response can be stored to later serve another 321 * request. 322 */ isCacheable(RequestHeaders request)323 public boolean isCacheable(RequestHeaders request) { 324 /* 325 * Always go to network for uncacheable response codes (RFC 2616, 13.4), 326 * This implementation doesn't support caching partial content. 327 */ 328 int responseCode = headers.getResponseCode(); 329 if (responseCode != HttpURLConnection.HTTP_OK 330 && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE 331 && responseCode != HttpURLConnection.HTTP_MULT_CHOICE 332 && responseCode != HttpURLConnection.HTTP_MOVED_PERM 333 && responseCode != HttpURLConnection.HTTP_GONE) { 334 return false; 335 } 336 337 /* 338 * Responses to authorized requests aren't cacheable unless they include 339 * a 'public', 'must-revalidate' or 's-maxage' directive. 340 */ 341 if (request.hasAuthorization() 342 && !isPublic 343 && !mustRevalidate 344 && sMaxAgeSeconds == -1) { 345 return false; 346 } 347 348 if (noStore) { 349 return false; 350 } 351 352 return true; 353 } 354 355 /** 356 * Returns true if a Vary header contains an asterisk. Such responses cannot 357 * be cached. 358 */ hasVaryAll()359 public boolean hasVaryAll() { 360 return varyFields.contains("*"); 361 } 362 363 /** 364 * Returns true if none of the Vary headers on this response have changed 365 * between {@code cachedRequest} and {@code newRequest}. 366 */ varyMatches(Map<String, List<String>> cachedRequest, Map<String, List<String>> newRequest)367 public boolean varyMatches(Map<String, List<String>> cachedRequest, 368 Map<String, List<String>> newRequest) { 369 for (String field : varyFields) { 370 if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) { 371 return false; 372 } 373 } 374 return true; 375 } 376 377 /** 378 * Returns the source to satisfy {@code request} given this cached response. 379 */ chooseResponseSource(long nowMillis, RequestHeaders request)380 public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) { 381 /* 382 * If this response shouldn't have been stored, it should never be used 383 * as a response source. This check should be redundant as long as the 384 * persistence store is well-behaved and the rules are constant. 385 */ 386 if (!isCacheable(request)) { 387 return ResponseSource.NETWORK; 388 } 389 390 if (request.isNoCache() || request.hasConditions()) { 391 return ResponseSource.NETWORK; 392 } 393 394 long ageMillis = computeAge(nowMillis); 395 long freshMillis = computeFreshnessLifetime(); 396 397 if (request.getMaxAgeSeconds() != -1) { 398 freshMillis = Math.min(freshMillis, 399 TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); 400 } 401 402 long minFreshMillis = 0; 403 if (request.getMinFreshSeconds() != -1) { 404 minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); 405 } 406 407 long maxStaleMillis = 0; 408 if (!mustRevalidate && request.getMaxStaleSeconds() != -1) { 409 maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); 410 } 411 412 if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { 413 if (ageMillis + minFreshMillis >= freshMillis) { 414 headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); 415 } 416 if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) { 417 headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); 418 } 419 return ResponseSource.CACHE; 420 } 421 422 if (lastModified != null) { 423 request.setIfModifiedSince(lastModified); 424 } else if (servedDate != null) { 425 request.setIfModifiedSince(servedDate); 426 } 427 428 if (etag != null) { 429 request.setIfNoneMatch(etag); 430 } 431 432 return request.hasConditions() 433 ? ResponseSource.CONDITIONAL_CACHE 434 : ResponseSource.NETWORK; 435 } 436 437 /** 438 * Returns true if this cached response should be used; false if the 439 * network response should be used. 440 */ validate(ResponseHeaders networkResponse)441 public boolean validate(ResponseHeaders networkResponse) { 442 if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { 443 return true; 444 } 445 446 /* 447 * The HTTP spec says that if the network's response is older than our 448 * cached response, we may return the cache's response. Like Chrome (but 449 * unlike Firefox), this client prefers to return the newer response. 450 */ 451 if (lastModified != null 452 && networkResponse.lastModified != null 453 && networkResponse.lastModified.getTime() < lastModified.getTime()) { 454 return true; 455 } 456 457 return false; 458 } 459 460 /** 461 * Combines this cached header with a network header as defined by RFC 2616, 462 * 13.5.3. 463 */ combine(ResponseHeaders network)464 public ResponseHeaders combine(ResponseHeaders network) { 465 RawHeaders result = new RawHeaders(); 466 result.setStatusLine(headers.getStatusLine()); 467 468 for (int i = 0; i < headers.length(); i++) { 469 String fieldName = headers.getFieldName(i); 470 String value = headers.getValue(i); 471 if (fieldName.equals("Warning") && value.startsWith("1")) { 472 continue; // drop 100-level freshness warnings 473 } 474 if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) { 475 result.add(fieldName, value); 476 } 477 } 478 479 for (int i = 0; i < network.headers.length(); i++) { 480 String fieldName = network.headers.getFieldName(i); 481 if (isEndToEnd(fieldName)) { 482 result.add(fieldName, network.headers.getValue(i)); 483 } 484 } 485 486 return new ResponseHeaders(uri, result); 487 } 488 489 /** 490 * Returns true if {@code fieldName} is an end-to-end HTTP header, as 491 * defined by RFC 2616, 13.5.1. 492 */ isEndToEnd(String fieldName)493 private static boolean isEndToEnd(String fieldName) { 494 return !fieldName.equalsIgnoreCase("Connection") 495 && !fieldName.equalsIgnoreCase("Keep-Alive") 496 && !fieldName.equalsIgnoreCase("Proxy-Authenticate") 497 && !fieldName.equalsIgnoreCase("Proxy-Authorization") 498 && !fieldName.equalsIgnoreCase("TE") 499 && !fieldName.equalsIgnoreCase("Trailers") 500 && !fieldName.equalsIgnoreCase("Transfer-Encoding") 501 && !fieldName.equalsIgnoreCase("Upgrade"); 502 } 503 } 504