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 17package com.squareup.okhttp; 18 19import com.squareup.okhttp.internal.DiskLruCache; 20import com.squareup.okhttp.internal.Util; 21import com.squareup.okhttp.internal.http.HttpMethod; 22import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; 23import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; 24import com.squareup.okhttp.internal.http.JavaApiConverter; 25import java.io.BufferedWriter; 26import java.io.ByteArrayInputStream; 27import java.io.File; 28import java.io.FilterInputStream; 29import java.io.FilterOutputStream; 30import java.io.IOException; 31import java.io.InputStream; 32import java.io.OutputStream; 33import java.io.OutputStreamWriter; 34import java.io.Writer; 35import java.net.CacheRequest; 36import java.net.CacheResponse; 37import java.net.ResponseCache; 38import java.net.URI; 39import java.net.URLConnection; 40import java.security.cert.Certificate; 41import java.security.cert.CertificateEncodingException; 42import java.security.cert.CertificateException; 43import java.security.cert.CertificateFactory; 44import java.util.ArrayList; 45import java.util.Collections; 46import java.util.List; 47import java.util.Map; 48import okio.BufferedSource; 49import okio.ByteString; 50import okio.Okio; 51 52import static com.squareup.okhttp.internal.Util.UTF_8; 53 54/** 55 * Caches HTTP and HTTPS responses to the filesystem so they may be reused, 56 * saving time and bandwidth. 57 * 58 * <p>This cache extends {@link ResponseCache} but is only intended for use 59 * with OkHttp and is not a general-purpose implementation: The 60 * {@link ResponseCache} API requires that the subclass handles cache-control 61 * logic as well as storage. In OkHttp the {@link HttpResponseCache} only 62 * handles cursory cache-control logic. 63 * 64 * <p>To maintain support for previous releases the {@link HttpResponseCache} 65 * will disregard any {@link #put(java.net.URI, java.net.URLConnection)} 66 * calls with a URLConnection that is not from OkHttp. It will, however, 67 * return cached data for any calls to {@link #get(java.net.URI, String, 68 * java.util.Map)}. 69 * 70 * <h3>Cache Optimization</h3> 71 * To measure cache effectiveness, this class tracks three statistics: 72 * <ul> 73 * <li><strong>{@link #getRequestCount() Request Count:}</strong> the number 74 * of HTTP requests issued since this cache was created. 75 * <li><strong>{@link #getNetworkCount() Network Count:}</strong> the 76 * number of those requests that required network use. 77 * <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of 78 * those requests whose responses were served by the cache. 79 * </ul> 80 * Sometimes a request will result in a conditional cache hit. If the cache 81 * contains a stale copy of the response, the client will issue a conditional 82 * {@code GET}. The server will then send either the updated response if it has 83 * changed, or a short 'not modified' response if the client's copy is still 84 * valid. Such responses increment both the network count and hit count. 85 * 86 * <p>The best way to improve the cache hit rate is by configuring the web 87 * server to return cacheable responses. Although this client honors all <a 88 * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache 89 * headers, it doesn't cache partial responses. 90 * 91 * <h3>Force a Network Response</h3> 92 * In some situations, such as after a user clicks a 'refresh' button, it may be 93 * necessary to skip the cache, and fetch data directly from the server. To force 94 * a full refresh, add the {@code no-cache} directive: <pre> {@code 95 * connection.addRequestProperty("Cache-Control", "no-cache"); 96 * }</pre> 97 * If it is only necessary to force a cached response to be validated by the 98 * server, use the more efficient {@code max-age=0} instead: <pre> {@code 99 * connection.addRequestProperty("Cache-Control", "max-age=0"); 100 * }</pre> 101 * 102 * <h3>Force a Cache Response</h3> 103 * Sometimes you'll want to show resources if they are available immediately, 104 * but not otherwise. This can be used so your application can show 105 * <i>something</i> while waiting for the latest data to be downloaded. To 106 * restrict a request to locally-cached resources, add the {@code 107 * only-if-cached} directive: <pre> {@code 108 * try { 109 * connection.addRequestProperty("Cache-Control", "only-if-cached"); 110 * InputStream cached = connection.getInputStream(); 111 * // the resource was cached! show it 112 * } catch (FileNotFoundException e) { 113 * // the resource was not cached 114 * } 115 * }</pre> 116 * This technique works even better in situations where a stale response is 117 * better than no response. To permit stale cached responses, use the {@code 118 * max-stale} directive with the maximum staleness in seconds: <pre> {@code 119 * int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale 120 * connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale); 121 * }</pre> 122 */ 123public final class HttpResponseCache extends ResponseCache implements OkResponseCache { 124 // TODO: add APIs to iterate the cache? 125 private static final int VERSION = 201105; 126 private static final int ENTRY_METADATA = 0; 127 private static final int ENTRY_BODY = 1; 128 private static final int ENTRY_COUNT = 2; 129 130 private final DiskLruCache cache; 131 132 /* read and write statistics, all guarded by 'this' */ 133 private int writeSuccessCount; 134 private int writeAbortCount; 135 private int networkCount; 136 private int hitCount; 137 private int requestCount; 138 139 public HttpResponseCache(File directory, long maxSize) throws IOException { 140 cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); 141 } 142 143 @Override public CacheResponse get( 144 URI uri, String requestMethod, Map<String, List<String>> requestHeaders) 145 throws IOException { 146 147 Request request = JavaApiConverter.createOkRequest(uri, requestMethod, requestHeaders); 148 Response response = get(request); 149 if (response == null) { 150 return null; 151 } 152 return JavaApiConverter.createJavaCacheResponse(response); 153 } 154 155 private static String urlToKey(Request requst) { 156 return Util.hash(requst.urlString()); 157 } 158 159 @Override public Response get(Request request) { 160 String key = urlToKey(request); 161 DiskLruCache.Snapshot snapshot; 162 Entry entry; 163 try { 164 snapshot = cache.get(key); 165 if (snapshot == null) { 166 return null; 167 } 168 entry = new Entry(snapshot.getInputStream(ENTRY_METADATA)); 169 } catch (IOException e) { 170 // Give up because the cache cannot be read. 171 return null; 172 } 173 174 Response response = entry.response(request, snapshot); 175 176 if (!entry.matches(request, response)) { 177 Util.closeQuietly(response.body()); 178 return null; 179 } 180 181 return response; 182 } 183 184 @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { 185 if (!isCacheableConnection(urlConnection)) { 186 return null; 187 } 188 return put(JavaApiConverter.createOkResponse(uri, urlConnection)); 189 } 190 191 private static boolean isCacheableConnection(URLConnection httpConnection) { 192 return (httpConnection instanceof HttpURLConnectionImpl) 193 || (httpConnection instanceof HttpsURLConnectionImpl); 194 } 195 196 @Override public CacheRequest put(Response response) throws IOException { 197 String requestMethod = response.request().method(); 198 199 if (maybeRemove(response.request())) { 200 return null; 201 } 202 if (!requestMethod.equals("GET")) { 203 // Don't cache non-GET responses. We're technically allowed to cache 204 // HEAD requests and some POST requests, but the complexity of doing 205 // so is high and the benefit is low. 206 return null; 207 } 208 209 if (response.hasVaryAll()) { 210 return null; 211 } 212 213 Entry entry = new Entry(response); 214 DiskLruCache.Editor editor = null; 215 try { 216 editor = cache.edit(urlToKey(response.request())); 217 if (editor == null) { 218 return null; 219 } 220 entry.writeTo(editor); 221 return new CacheRequestImpl(editor); 222 } catch (IOException e) { 223 abortQuietly(editor); 224 return null; 225 } 226 } 227 228 @Override public boolean maybeRemove(Request request) { 229 if (HttpMethod.invalidatesCache(request.method())) { 230 try { 231 cache.remove(urlToKey(request)); 232 } catch (IOException ignored) { 233 // The cache cannot be written. 234 } 235 return true; 236 } 237 return false; 238 } 239 240 @Override public void update(Response cached, Response network) { 241 Entry entry = new Entry(network); 242 DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; 243 DiskLruCache.Editor editor = null; 244 try { 245 editor = snapshot.edit(); // Returns null if snapshot is not current. 246 if (editor != null) { 247 entry.writeTo(editor); 248 editor.commit(); 249 } 250 } catch (IOException e) { 251 abortQuietly(editor); 252 } 253 } 254 255 private void abortQuietly(DiskLruCache.Editor editor) { 256 // Give up because the cache cannot be written. 257 try { 258 if (editor != null) { 259 editor.abort(); 260 } 261 } catch (IOException ignored) { 262 } 263 } 264 265 /** 266 * Closes the cache and deletes all of its stored values. This will delete 267 * all files in the cache directory including files that weren't created by 268 * the cache. 269 */ 270 public void delete() throws IOException { 271 cache.delete(); 272 } 273 274 public synchronized int getWriteAbortCount() { 275 return writeAbortCount; 276 } 277 278 public synchronized int getWriteSuccessCount() { 279 return writeSuccessCount; 280 } 281 282 public long getSize() { 283 return cache.size(); 284 } 285 286 public long getMaxSize() { 287 return cache.getMaxSize(); 288 } 289 290 public void flush() throws IOException { 291 cache.flush(); 292 } 293 294 public void close() throws IOException { 295 cache.close(); 296 } 297 298 public File getDirectory() { 299 return cache.getDirectory(); 300 } 301 302 public boolean isClosed() { 303 return cache.isClosed(); 304 } 305 306 @Override public synchronized void trackResponse(ResponseSource source) { 307 requestCount++; 308 309 switch (source) { 310 case CACHE: 311 hitCount++; 312 break; 313 case CONDITIONAL_CACHE: 314 case NETWORK: 315 networkCount++; 316 break; 317 } 318 } 319 320 @Override public synchronized void trackConditionalCacheHit() { 321 hitCount++; 322 } 323 324 public synchronized int getNetworkCount() { 325 return networkCount; 326 } 327 328 public synchronized int getHitCount() { 329 return hitCount; 330 } 331 332 public synchronized int getRequestCount() { 333 return requestCount; 334 } 335 336 private final class CacheRequestImpl extends CacheRequest { 337 private final DiskLruCache.Editor editor; 338 private OutputStream cacheOut; 339 private boolean done; 340 private OutputStream body; 341 342 public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { 343 this.editor = editor; 344 this.cacheOut = editor.newOutputStream(ENTRY_BODY); 345 this.body = new FilterOutputStream(cacheOut) { 346 @Override public void close() throws IOException { 347 synchronized (HttpResponseCache.this) { 348 if (done) { 349 return; 350 } 351 done = true; 352 writeSuccessCount++; 353 } 354 super.close(); 355 editor.commit(); 356 } 357 358 @Override public void write(byte[] buffer, int offset, int length) throws IOException { 359 // Since we don't override "write(int oneByte)", we can write directly to "out" 360 // and avoid the inefficient implementation from the FilterOutputStream. 361 out.write(buffer, offset, length); 362 } 363 }; 364 } 365 366 @Override public void abort() { 367 synchronized (HttpResponseCache.this) { 368 if (done) { 369 return; 370 } 371 done = true; 372 writeAbortCount++; 373 } 374 Util.closeQuietly(cacheOut); 375 try { 376 editor.abort(); 377 } catch (IOException ignored) { 378 } 379 } 380 381 @Override public OutputStream getBody() throws IOException { 382 return body; 383 } 384 } 385 386 private static final class Entry { 387 private final String url; 388 private final Headers varyHeaders; 389 private final String requestMethod; 390 private final String statusLine; 391 private final Headers responseHeaders; 392 private final Handshake handshake; 393 394 /** 395 * Reads an entry from an input stream. A typical entry looks like this: 396 * <pre>{@code 397 * http://google.com/foo 398 * GET 399 * 2 400 * Accept-Language: fr-CA 401 * Accept-Charset: UTF-8 402 * HTTP/1.1 200 OK 403 * 3 404 * Content-Type: image/png 405 * Content-Length: 100 406 * Cache-Control: max-age=600 407 * }</pre> 408 * 409 * <p>A typical HTTPS file looks like this: 410 * <pre>{@code 411 * https://google.com/foo 412 * GET 413 * 2 414 * Accept-Language: fr-CA 415 * Accept-Charset: UTF-8 416 * HTTP/1.1 200 OK 417 * 3 418 * Content-Type: image/png 419 * Content-Length: 100 420 * Cache-Control: max-age=600 421 * 422 * AES_256_WITH_MD5 423 * 2 424 * base64-encoded peerCertificate[0] 425 * base64-encoded peerCertificate[1] 426 * -1 427 * }</pre> 428 * The file is newline separated. The first two lines are the URL and 429 * the request method. Next is the number of HTTP Vary request header 430 * lines, followed by those lines. 431 * 432 * <p>Next is the response status line, followed by the number of HTTP 433 * response header lines, followed by those lines. 434 * 435 * <p>HTTPS responses also contain SSL session information. This begins 436 * with a blank line, and then a line containing the cipher suite. Next 437 * is the length of the peer certificate chain. These certificates are 438 * base64-encoded and appear each on their own line. The next line 439 * contains the length of the local certificate chain. These 440 * certificates are also base64-encoded and appear each on their own 441 * line. A length of -1 is used to encode a null array. 442 */ 443 public Entry(InputStream in) throws IOException { 444 try { 445 BufferedSource source = Okio.buffer(Okio.source(in)); 446 url = source.readUtf8LineStrict(); 447 requestMethod = source.readUtf8LineStrict(); 448 Headers.Builder varyHeadersBuilder = new Headers.Builder(); 449 int varyRequestHeaderLineCount = readInt(source); 450 for (int i = 0; i < varyRequestHeaderLineCount; i++) { 451 varyHeadersBuilder.addLine(source.readUtf8LineStrict()); 452 } 453 varyHeaders = varyHeadersBuilder.build(); 454 455 statusLine = source.readUtf8LineStrict(); 456 Headers.Builder responseHeadersBuilder = new Headers.Builder(); 457 int responseHeaderLineCount = readInt(source); 458 for (int i = 0; i < responseHeaderLineCount; i++) { 459 responseHeadersBuilder.addLine(source.readUtf8LineStrict()); 460 } 461 responseHeaders = responseHeadersBuilder.build(); 462 463 if (isHttps()) { 464 String blank = source.readUtf8LineStrict(); 465 if (blank.length() > 0) { 466 throw new IOException("expected \"\" but was \"" + blank + "\""); 467 } 468 String cipherSuite = source.readUtf8LineStrict(); 469 List<Certificate> peerCertificates = readCertificateList(source); 470 List<Certificate> localCertificates = readCertificateList(source); 471 handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates); 472 } else { 473 handshake = null; 474 } 475 } finally { 476 in.close(); 477 } 478 } 479 480 public Entry(Response response) { 481 this.url = response.request().urlString(); 482 this.varyHeaders = response.request().headers().getAll(response.getVaryFields()); 483 this.requestMethod = response.request().method(); 484 this.statusLine = response.statusLine(); 485 this.responseHeaders = response.headers(); 486 this.handshake = response.handshake(); 487 } 488 489 public void writeTo(DiskLruCache.Editor editor) throws IOException { 490 OutputStream out = editor.newOutputStream(ENTRY_METADATA); 491 Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8)); 492 493 writer.write(url + '\n'); 494 writer.write(requestMethod + '\n'); 495 writer.write(Integer.toString(varyHeaders.size()) + '\n'); 496 for (int i = 0; i < varyHeaders.size(); i++) { 497 writer.write(varyHeaders.name(i) + ": " + varyHeaders.value(i) + '\n'); 498 } 499 500 writer.write(statusLine + '\n'); 501 writer.write(Integer.toString(responseHeaders.size()) + '\n'); 502 for (int i = 0; i < responseHeaders.size(); i++) { 503 writer.write(responseHeaders.name(i) + ": " + responseHeaders.value(i) + '\n'); 504 } 505 506 if (isHttps()) { 507 writer.write('\n'); 508 writer.write(handshake.cipherSuite() + '\n'); 509 writeCertArray(writer, handshake.peerCertificates()); 510 writeCertArray(writer, handshake.localCertificates()); 511 } 512 writer.close(); 513 } 514 515 private boolean isHttps() { 516 return url.startsWith("https://"); 517 } 518 519 private List<Certificate> readCertificateList(BufferedSource source) throws IOException { 520 int length = readInt(source); 521 if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null. 522 523 try { 524 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 525 List<Certificate> result = new ArrayList<Certificate>(length); 526 for (int i = 0; i < length; i++) { 527 String line = source.readUtf8LineStrict(); 528 byte[] bytes = ByteString.decodeBase64(line).toByteArray(); 529 result.add(certificateFactory.generateCertificate(new ByteArrayInputStream(bytes))); 530 } 531 return result; 532 } catch (CertificateException e) { 533 throw new IOException(e.getMessage()); 534 } 535 } 536 537 private void writeCertArray(Writer writer, List<Certificate> certificates) throws IOException { 538 try { 539 writer.write(Integer.toString(certificates.size()) + '\n'); 540 for (int i = 0, size = certificates.size(); i < size; i++) { 541 byte[] bytes = certificates.get(i).getEncoded(); 542 String line = ByteString.of(bytes).base64(); 543 writer.write(line + '\n'); 544 } 545 } catch (CertificateEncodingException e) { 546 throw new IOException(e.getMessage()); 547 } 548 } 549 550 public boolean matches(Request request, Response response) { 551 return url.equals(request.urlString()) 552 && requestMethod.equals(request.method()) 553 && response.varyMatches(varyHeaders, request); 554 } 555 556 public Response response(Request request, DiskLruCache.Snapshot snapshot) { 557 String contentType = responseHeaders.get("Content-Type"); 558 String contentLength = responseHeaders.get("Content-Length"); 559 return new Response.Builder() 560 .request(request) 561 .statusLine(statusLine) 562 .headers(responseHeaders) 563 .body(new CacheResponseBody(snapshot, contentType, contentLength)) 564 .handshake(handshake) 565 .build(); 566 } 567 } 568 569 private static int readInt(BufferedSource source) throws IOException { 570 String line = source.readUtf8LineStrict(); 571 try { 572 return Integer.parseInt(line); 573 } catch (NumberFormatException e) { 574 throw new IOException("Expected an integer but was \"" + line + "\""); 575 } 576 } 577 578 private static class CacheResponseBody extends Response.Body { 579 private final DiskLruCache.Snapshot snapshot; 580 private final InputStream bodyIn; 581 private final String contentType; 582 private final String contentLength; 583 584 public CacheResponseBody(final DiskLruCache.Snapshot snapshot, 585 String contentType, String contentLength) { 586 this.snapshot = snapshot; 587 this.contentType = contentType; 588 this.contentLength = contentLength; 589 590 // This input stream closes the snapshot when the stream is closed. 591 this.bodyIn = new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) { 592 @Override public void close() throws IOException { 593 snapshot.close(); 594 super.close(); 595 } 596 }; 597 } 598 599 @Override public boolean ready() throws IOException { 600 return true; 601 } 602 603 @Override public MediaType contentType() { 604 return contentType != null ? MediaType.parse(contentType) : null; 605 } 606 607 @Override public long contentLength() { 608 try { 609 return contentLength != null ? Long.parseLong(contentLength) : -1; 610 } catch (NumberFormatException e) { 611 return -1; 612 } 613 } 614 615 @Override public InputStream byteStream() { 616 return bodyIn; 617 } 618 } 619} 620