1 /* 2 * Copyright (C) 2010 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; 18 19 import com.squareup.okhttp.internal.DiskLruCache; 20 import com.squareup.okhttp.internal.InternalCache; 21 import com.squareup.okhttp.internal.Util; 22 import com.squareup.okhttp.internal.http.CacheRequest; 23 import com.squareup.okhttp.internal.http.CacheStrategy; 24 import com.squareup.okhttp.internal.http.HttpMethod; 25 import com.squareup.okhttp.internal.http.OkHeaders; 26 import com.squareup.okhttp.internal.http.StatusLine; 27 import com.squareup.okhttp.internal.io.FileSystem; 28 import java.io.File; 29 import java.io.IOException; 30 import java.security.cert.Certificate; 31 import java.security.cert.CertificateEncodingException; 32 import java.security.cert.CertificateException; 33 import java.security.cert.CertificateFactory; 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.Iterator; 37 import java.util.List; 38 import java.util.NoSuchElementException; 39 import okio.Buffer; 40 import okio.BufferedSink; 41 import okio.BufferedSource; 42 import okio.ByteString; 43 import okio.ForwardingSink; 44 import okio.ForwardingSource; 45 import okio.Okio; 46 import okio.Sink; 47 import okio.Source; 48 49 /** 50 * Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and 51 * bandwidth. 52 * 53 * <h3>Cache Optimization</h3> 54 * To measure cache effectiveness, this class tracks three statistics: 55 * <ul> 56 * <li><strong>{@linkplain #getRequestCount() Request Count:}</strong> the number of HTTP 57 * requests issued since this cache was created. 58 * <li><strong>{@linkplain #getNetworkCount() Network Count:}</strong> the number of those 59 * requests that required network use. 60 * <li><strong>{@linkplain #getHitCount() Hit Count:}</strong> the number of those requests whose 61 * responses were served by the cache. 62 * </ul> 63 * 64 * Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of 65 * the response, the client will issue a conditional {@code GET}. The server will then send either 66 * the updated response if it has changed, or a short 'not modified' response if the client's copy 67 * is still valid. Such responses increment both the network count and hit count. 68 * 69 * <p>The best way to improve the cache hit rate is by configuring the web server to return 70 * cacheable responses. Although this client honors all <a 71 * href="http://tools.ietf.org/html/rfc7234">HTTP/1.1 (RFC 7234)</a> cache headers, it doesn't cache 72 * partial responses. 73 * 74 * <h3>Force a Network Response</h3> 75 * In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip 76 * the cache, and fetch data directly from the server. To force a full refresh, add the {@code 77 * no-cache} directive: <pre> {@code 78 * 79 * Request request = new Request.Builder() 80 * .cacheControl(new CacheControl.Builder().noCache().build()) 81 * .url("http://publicobject.com/helloworld.txt") 82 * .build(); 83 * }</pre> 84 * 85 * If it is only necessary to force a cached response to be validated by the server, use the more 86 * efficient {@code max-age=0} directive instead: <pre> {@code 87 * 88 * Request request = new Request.Builder() 89 * .cacheControl(new CacheControl.Builder() 90 * .maxAge(0, TimeUnit.SECONDS) 91 * .build()) 92 * .url("http://publicobject.com/helloworld.txt") 93 * .build(); 94 * }</pre> 95 * 96 * <h3>Force a Cache Response</h3> 97 * Sometimes you'll want to show resources if they are available immediately, but not otherwise. 98 * This can be used so your application can show <i>something</i> while waiting for the latest data 99 * to be downloaded. To restrict a request to locally-cached resources, add the {@code 100 * only-if-cached} directive: <pre> {@code 101 * 102 * Request request = new Request.Builder() 103 * .cacheControl(new CacheControl.Builder() 104 * .onlyIfCached() 105 * .build()) 106 * .url("http://publicobject.com/helloworld.txt") 107 * .build(); 108 * Response forceCacheResponse = client.newCall(request).execute(); 109 * if (forceCacheResponse.code() != 504) { 110 * // The resource was cached! Show it. 111 * } else { 112 * // The resource was not cached. 113 * } 114 * }</pre> 115 * This technique works even better in situations where a stale response is better than no response. 116 * To permit stale cached responses, use the {@code max-stale} directive with the maximum staleness 117 * in seconds: <pre> {@code 118 * 119 * Request request = new Request.Builder() 120 * .cacheControl(new CacheControl.Builder() 121 * .maxStale(365, TimeUnit.DAYS) 122 * .build()) 123 * .url("http://publicobject.com/helloworld.txt") 124 * .build(); 125 * }</pre> 126 * 127 * <p>The {@link CacheControl} class can configure request caching directives and parse response 128 * caching directives. It even offers convenient constants {@link CacheControl#FORCE_NETWORK} and 129 * {@link CacheControl#FORCE_CACHE} that address the use cases above. 130 */ 131 public final class Cache { 132 private static final int VERSION = 201105; 133 private static final int ENTRY_METADATA = 0; 134 private static final int ENTRY_BODY = 1; 135 private static final int ENTRY_COUNT = 2; 136 137 final InternalCache internalCache = new InternalCache() { 138 @Override public Response get(Request request) throws IOException { 139 return Cache.this.get(request); 140 } 141 @Override public CacheRequest put(Response response) throws IOException { 142 return Cache.this.put(response); 143 } 144 @Override public void remove(Request request) throws IOException { 145 Cache.this.remove(request); 146 } 147 @Override public void update(Response cached, Response network) throws IOException { 148 Cache.this.update(cached, network); 149 } 150 @Override public void trackConditionalCacheHit() { 151 Cache.this.trackConditionalCacheHit(); 152 } 153 @Override public void trackResponse(CacheStrategy cacheStrategy) { 154 Cache.this.trackResponse(cacheStrategy); 155 } 156 }; 157 158 private final DiskLruCache cache; 159 160 /* read and write statistics, all guarded by 'this' */ 161 private int writeSuccessCount; 162 private int writeAbortCount; 163 private int networkCount; 164 private int hitCount; 165 private int requestCount; 166 167 public Cache(File directory, long maxSize) { 168 this(directory, maxSize, FileSystem.SYSTEM); 169 } 170 171 Cache(File directory, long maxSize, FileSystem fileSystem) { 172 this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize); 173 } 174 175 private static String urlToKey(Request request) { 176 return Util.md5Hex(request.urlString()); 177 } 178 179 Response get(Request request) { 180 String key = urlToKey(request); 181 DiskLruCache.Snapshot snapshot; 182 Entry entry; 183 try { 184 snapshot = cache.get(key); 185 if (snapshot == null) { 186 return null; 187 } 188 } catch (IOException e) { 189 // Give up because the cache cannot be read. 190 return null; 191 } 192 193 try { 194 entry = new Entry(snapshot.getSource(ENTRY_METADATA)); 195 } catch (IOException e) { 196 Util.closeQuietly(snapshot); 197 return null; 198 } 199 200 Response response = entry.response(request, snapshot); 201 202 if (!entry.matches(request, response)) { 203 Util.closeQuietly(response.body()); 204 return null; 205 } 206 207 return response; 208 } 209 210 private CacheRequest put(Response response) throws IOException { 211 String requestMethod = response.request().method(); 212 213 if (HttpMethod.invalidatesCache(response.request().method())) { 214 try { 215 remove(response.request()); 216 } catch (IOException ignored) { 217 // The cache cannot be written. 218 } 219 return null; 220 } 221 if (!requestMethod.equals("GET")) { 222 // Don't cache non-GET responses. We're technically allowed to cache 223 // HEAD requests and some POST requests, but the complexity of doing 224 // so is high and the benefit is low. 225 return null; 226 } 227 228 if (OkHeaders.hasVaryAll(response)) { 229 return null; 230 } 231 232 Entry entry = new Entry(response); 233 DiskLruCache.Editor editor = null; 234 try { 235 editor = cache.edit(urlToKey(response.request())); 236 if (editor == null) { 237 return null; 238 } 239 entry.writeTo(editor); 240 return new CacheRequestImpl(editor); 241 } catch (IOException e) { 242 abortQuietly(editor); 243 return null; 244 } 245 } 246 247 private void remove(Request request) throws IOException { 248 cache.remove(urlToKey(request)); 249 } 250 251 private void update(Response cached, Response network) { 252 Entry entry = new Entry(network); 253 DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; 254 DiskLruCache.Editor editor = null; 255 try { 256 editor = snapshot.edit(); // Returns null if snapshot is not current. 257 if (editor != null) { 258 entry.writeTo(editor); 259 editor.commit(); 260 } 261 } catch (IOException e) { 262 abortQuietly(editor); 263 } 264 } 265 266 private void abortQuietly(DiskLruCache.Editor editor) { 267 // Give up because the cache cannot be written. 268 try { 269 if (editor != null) { 270 editor.abort(); 271 } 272 } catch (IOException ignored) { 273 } 274 } 275 276 /** 277 * Initialize the cache. This will include reading the journal files from 278 * the storage and building up the necessary in-memory cache information. 279 * <p> 280 * The initialization time may vary depending on the journal file size and 281 * the current actual cache size. The application needs to be aware of calling 282 * this function during the initialization phase and preferably in a background 283 * worker thread. 284 * <p> 285 * Note that if the application chooses to not call this method to initialize 286 * the cache. By default, the okhttp will perform lazy initialization upon the 287 * first usage of the cache. 288 */ 289 public void initialize() throws IOException { 290 cache.initialize(); 291 } 292 293 /** 294 * Closes the cache and deletes all of its stored values. This will delete 295 * all files in the cache directory including files that weren't created by 296 * the cache. 297 */ 298 public void delete() throws IOException { 299 cache.delete(); 300 } 301 302 /** 303 * Deletes all values stored in the cache. In-flight writes to the cache will 304 * complete normally, but the corresponding responses will not be stored. 305 */ 306 public void evictAll() throws IOException { 307 cache.evictAll(); 308 } 309 310 /** 311 * Returns an iterator over the URLs in this cache. This iterator doesn't throw {@code 312 * ConcurrentModificationException}, but if new responses are added while iterating, their URLs 313 * will not be returned. If existing responses are evicted during iteration, they will be absent 314 * (unless they were already returned). 315 * 316 * <p>The iterator supports {@linkplain Iterator#remove}. Removing a URL from the iterator evicts 317 * the corresponding response from the cache. Use this to evict selected responses. 318 */ 319 public Iterator<String> urls() throws IOException { 320 return new Iterator<String>() { 321 final Iterator<DiskLruCache.Snapshot> delegate = cache.snapshots(); 322 323 String nextUrl; 324 boolean canRemove; 325 326 @Override public boolean hasNext() { 327 if (nextUrl != null) return true; 328 329 canRemove = false; // Prevent delegate.remove() on the wrong item! 330 while (delegate.hasNext()) { 331 DiskLruCache.Snapshot snapshot = delegate.next(); 332 try { 333 BufferedSource metadata = Okio.buffer(snapshot.getSource(ENTRY_METADATA)); 334 nextUrl = metadata.readUtf8LineStrict(); 335 return true; 336 } catch (IOException ignored) { 337 // We couldn't read the metadata for this snapshot; possibly because the host filesystem 338 // has disappeared! Skip it. 339 } finally { 340 snapshot.close(); 341 } 342 } 343 344 return false; 345 } 346 347 @Override public String next() { 348 if (!hasNext()) throw new NoSuchElementException(); 349 String result = nextUrl; 350 nextUrl = null; 351 canRemove = true; 352 return result; 353 } 354 355 @Override public void remove() { 356 if (!canRemove) throw new IllegalStateException("remove() before next()"); 357 delegate.remove(); 358 } 359 }; 360 } 361 362 public synchronized int getWriteAbortCount() { 363 return writeAbortCount; 364 } 365 366 public synchronized int getWriteSuccessCount() { 367 return writeSuccessCount; 368 } 369 370 public long getSize() throws IOException { 371 return cache.size(); 372 } 373 374 public long getMaxSize() { 375 return cache.getMaxSize(); 376 } 377 378 public void flush() throws IOException { 379 cache.flush(); 380 } 381 382 public void close() throws IOException { 383 cache.close(); 384 } 385 386 public File getDirectory() { 387 return cache.getDirectory(); 388 } 389 390 public boolean isClosed() { 391 return cache.isClosed(); 392 } 393 394 private synchronized void trackResponse(CacheStrategy cacheStrategy) { 395 requestCount++; 396 397 if (cacheStrategy.networkRequest != null) { 398 // If this is a conditional request, we'll increment hitCount if/when it hits. 399 networkCount++; 400 401 } else if (cacheStrategy.cacheResponse != null) { 402 // This response uses the cache and not the network. That's a cache hit. 403 hitCount++; 404 } 405 } 406 407 private synchronized void trackConditionalCacheHit() { 408 hitCount++; 409 } 410 411 public synchronized int getNetworkCount() { 412 return networkCount; 413 } 414 415 public synchronized int getHitCount() { 416 return hitCount; 417 } 418 419 public synchronized int getRequestCount() { 420 return requestCount; 421 } 422 423 private final class CacheRequestImpl implements CacheRequest { 424 private final DiskLruCache.Editor editor; 425 private Sink cacheOut; 426 private boolean done; 427 private Sink body; 428 429 public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { 430 this.editor = editor; 431 this.cacheOut = editor.newSink(ENTRY_BODY); 432 this.body = new ForwardingSink(cacheOut) { 433 @Override public void close() throws IOException { 434 synchronized (Cache.this) { 435 if (done) { 436 return; 437 } 438 done = true; 439 writeSuccessCount++; 440 } 441 super.close(); 442 editor.commit(); 443 } 444 }; 445 } 446 447 @Override public void abort() { 448 synchronized (Cache.this) { 449 if (done) { 450 return; 451 } 452 done = true; 453 writeAbortCount++; 454 } 455 Util.closeQuietly(cacheOut); 456 try { 457 editor.abort(); 458 } catch (IOException ignored) { 459 } 460 } 461 462 @Override public Sink body() { 463 return body; 464 } 465 } 466 467 private static final class Entry { 468 private final String url; 469 private final Headers varyHeaders; 470 private final String requestMethod; 471 private final Protocol protocol; 472 private final int code; 473 private final String message; 474 private final Headers responseHeaders; 475 private final Handshake handshake; 476 477 /** 478 * Reads an entry from an input stream. A typical entry looks like this: 479 * <pre>{@code 480 * http://google.com/foo 481 * GET 482 * 2 483 * Accept-Language: fr-CA 484 * Accept-Charset: UTF-8 485 * HTTP/1.1 200 OK 486 * 3 487 * Content-Type: image/png 488 * Content-Length: 100 489 * Cache-Control: max-age=600 490 * }</pre> 491 * 492 * <p>A typical HTTPS file looks like this: 493 * <pre>{@code 494 * https://google.com/foo 495 * GET 496 * 2 497 * Accept-Language: fr-CA 498 * Accept-Charset: UTF-8 499 * HTTP/1.1 200 OK 500 * 3 501 * Content-Type: image/png 502 * Content-Length: 100 503 * Cache-Control: max-age=600 504 * 505 * AES_256_WITH_MD5 506 * 2 507 * base64-encoded peerCertificate[0] 508 * base64-encoded peerCertificate[1] 509 * -1 510 * }</pre> 511 * The file is newline separated. The first two lines are the URL and 512 * the request method. Next is the number of HTTP Vary request header 513 * lines, followed by those lines. 514 * 515 * <p>Next is the response status line, followed by the number of HTTP 516 * response header lines, followed by those lines. 517 * 518 * <p>HTTPS responses also contain SSL session information. This begins 519 * with a blank line, and then a line containing the cipher suite. Next 520 * is the length of the peer certificate chain. These certificates are 521 * base64-encoded and appear each on their own line. The next line 522 * contains the length of the local certificate chain. These 523 * certificates are also base64-encoded and appear each on their own 524 * line. A length of -1 is used to encode a null array. 525 */ 526 public Entry(Source in) throws IOException { 527 try { 528 BufferedSource source = Okio.buffer(in); 529 url = source.readUtf8LineStrict(); 530 requestMethod = source.readUtf8LineStrict(); 531 Headers.Builder varyHeadersBuilder = new Headers.Builder(); 532 int varyRequestHeaderLineCount = readInt(source); 533 for (int i = 0; i < varyRequestHeaderLineCount; i++) { 534 varyHeadersBuilder.addLenient(source.readUtf8LineStrict()); 535 } 536 varyHeaders = varyHeadersBuilder.build(); 537 538 StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); 539 protocol = statusLine.protocol; 540 code = statusLine.code; 541 message = statusLine.message; 542 Headers.Builder responseHeadersBuilder = new Headers.Builder(); 543 int responseHeaderLineCount = readInt(source); 544 for (int i = 0; i < responseHeaderLineCount; i++) { 545 responseHeadersBuilder.addLenient(source.readUtf8LineStrict()); 546 } 547 responseHeaders = responseHeadersBuilder.build(); 548 549 if (isHttps()) { 550 String blank = source.readUtf8LineStrict(); 551 if (blank.length() > 0) { 552 throw new IOException("expected \"\" but was \"" + blank + "\""); 553 } 554 String cipherSuite = source.readUtf8LineStrict(); 555 List<Certificate> peerCertificates = readCertificateList(source); 556 List<Certificate> localCertificates = readCertificateList(source); 557 handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates); 558 } else { 559 handshake = null; 560 } 561 } finally { 562 in.close(); 563 } 564 } 565 566 public Entry(Response response) { 567 this.url = response.request().urlString(); 568 this.varyHeaders = OkHeaders.varyHeaders(response); 569 this.requestMethod = response.request().method(); 570 this.protocol = response.protocol(); 571 this.code = response.code(); 572 this.message = response.message(); 573 this.responseHeaders = response.headers(); 574 this.handshake = response.handshake(); 575 } 576 577 public void writeTo(DiskLruCache.Editor editor) throws IOException { 578 BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA)); 579 580 sink.writeUtf8(url); 581 sink.writeByte('\n'); 582 sink.writeUtf8(requestMethod); 583 sink.writeByte('\n'); 584 sink.writeDecimalLong(varyHeaders.size()); 585 sink.writeByte('\n'); 586 for (int i = 0, size = varyHeaders.size(); i < size; i++) { 587 sink.writeUtf8(varyHeaders.name(i)); 588 sink.writeUtf8(": "); 589 sink.writeUtf8(varyHeaders.value(i)); 590 sink.writeByte('\n'); 591 } 592 593 sink.writeUtf8(new StatusLine(protocol, code, message).toString()); 594 sink.writeByte('\n'); 595 sink.writeDecimalLong(responseHeaders.size()); 596 sink.writeByte('\n'); 597 for (int i = 0, size = responseHeaders.size(); i < size; i++) { 598 sink.writeUtf8(responseHeaders.name(i)); 599 sink.writeUtf8(": "); 600 sink.writeUtf8(responseHeaders.value(i)); 601 sink.writeByte('\n'); 602 } 603 604 if (isHttps()) { 605 sink.writeByte('\n'); 606 sink.writeUtf8(handshake.cipherSuite()); 607 sink.writeByte('\n'); 608 writeCertList(sink, handshake.peerCertificates()); 609 writeCertList(sink, handshake.localCertificates()); 610 } 611 sink.close(); 612 } 613 614 private boolean isHttps() { 615 return url.startsWith("https://"); 616 } 617 618 private List<Certificate> readCertificateList(BufferedSource source) throws IOException { 619 int length = readInt(source); 620 if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null. 621 622 try { 623 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 624 List<Certificate> result = new ArrayList<>(length); 625 for (int i = 0; i < length; i++) { 626 String line = source.readUtf8LineStrict(); 627 Buffer bytes = new Buffer(); 628 bytes.write(ByteString.decodeBase64(line)); 629 result.add(certificateFactory.generateCertificate(bytes.inputStream())); 630 } 631 return result; 632 } catch (CertificateException e) { 633 throw new IOException(e.getMessage()); 634 } 635 } 636 637 private void writeCertList(BufferedSink sink, List<Certificate> certificates) 638 throws IOException { 639 try { 640 sink.writeDecimalLong(certificates.size()); 641 sink.writeByte('\n'); 642 for (int i = 0, size = certificates.size(); i < size; i++) { 643 byte[] bytes = certificates.get(i).getEncoded(); 644 String line = ByteString.of(bytes).base64(); 645 sink.writeUtf8(line); 646 sink.writeByte('\n'); 647 } 648 } catch (CertificateEncodingException e) { 649 throw new IOException(e.getMessage()); 650 } 651 } 652 653 public boolean matches(Request request, Response response) { 654 return url.equals(request.urlString()) 655 && requestMethod.equals(request.method()) 656 && OkHeaders.varyMatches(response, varyHeaders, request); 657 } 658 659 public Response response(Request request, DiskLruCache.Snapshot snapshot) { 660 String contentType = responseHeaders.get("Content-Type"); 661 String contentLength = responseHeaders.get("Content-Length"); 662 Request cacheRequest = new Request.Builder() 663 .url(url) 664 .method(requestMethod, null) 665 .headers(varyHeaders) 666 .build(); 667 return new Response.Builder() 668 .request(cacheRequest) 669 .protocol(protocol) 670 .code(code) 671 .message(message) 672 .headers(responseHeaders) 673 .body(new CacheResponseBody(snapshot, contentType, contentLength)) 674 .handshake(handshake) 675 .build(); 676 } 677 } 678 679 private static int readInt(BufferedSource source) throws IOException { 680 try { 681 long result = source.readDecimalLong(); 682 String line = source.readUtf8LineStrict(); 683 if (result < 0 || result > Integer.MAX_VALUE || !line.isEmpty()) { 684 throw new IOException("expected an int but was \"" + result + line + "\""); 685 } 686 return (int) result; 687 } catch (NumberFormatException e) { 688 throw new IOException(e.getMessage()); 689 } 690 } 691 692 private static class CacheResponseBody extends ResponseBody { 693 private final DiskLruCache.Snapshot snapshot; 694 private final BufferedSource bodySource; 695 private final String contentType; 696 private final String contentLength; 697 698 public CacheResponseBody(final DiskLruCache.Snapshot snapshot, 699 String contentType, String contentLength) { 700 this.snapshot = snapshot; 701 this.contentType = contentType; 702 this.contentLength = contentLength; 703 704 Source source = snapshot.getSource(ENTRY_BODY); 705 bodySource = Okio.buffer(new ForwardingSource(source) { 706 @Override public void close() throws IOException { 707 snapshot.close(); 708 super.close(); 709 } 710 }); 711 } 712 713 @Override public MediaType contentType() { 714 return contentType != null ? MediaType.parse(contentType) : null; 715 } 716 717 @Override public long contentLength() { 718 try { 719 return contentLength != null ? Long.parseLong(contentLength) : -1; 720 } catch (NumberFormatException e) { 721 return -1; 722 } 723 } 724 725 @Override public BufferedSource source() { 726 return bodySource; 727 } 728 } 729 } 730