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.Base64; 20import com.squareup.okhttp.internal.DiskLruCache; 21import com.squareup.okhttp.internal.StrictLineReader; 22import com.squareup.okhttp.internal.Util; 23import com.squareup.okhttp.internal.http.HttpEngine; 24import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; 25import com.squareup.okhttp.internal.http.HttpsEngine; 26import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; 27import com.squareup.okhttp.internal.http.RawHeaders; 28import com.squareup.okhttp.internal.http.ResponseHeaders; 29import java.io.BufferedWriter; 30import java.io.ByteArrayInputStream; 31import java.io.File; 32import java.io.FilterInputStream; 33import java.io.FilterOutputStream; 34import java.io.IOException; 35import java.io.InputStream; 36import java.io.OutputStream; 37import java.io.OutputStreamWriter; 38import java.io.UnsupportedEncodingException; 39import java.io.Writer; 40import java.net.CacheRequest; 41import java.net.CacheResponse; 42import java.net.HttpURLConnection; 43import java.net.ResponseCache; 44import java.net.SecureCacheResponse; 45import java.net.URI; 46import java.net.URLConnection; 47import java.security.MessageDigest; 48import java.security.NoSuchAlgorithmException; 49import java.security.Principal; 50import java.security.cert.Certificate; 51import java.security.cert.CertificateEncodingException; 52import java.security.cert.CertificateException; 53import java.security.cert.CertificateFactory; 54import java.security.cert.X509Certificate; 55import java.util.Arrays; 56import java.util.List; 57import java.util.Map; 58import javax.net.ssl.SSLPeerUnverifiedException; 59import javax.net.ssl.SSLSocket; 60 61import static com.squareup.okhttp.internal.Util.US_ASCII; 62import static com.squareup.okhttp.internal.Util.UTF_8; 63 64/** 65 * Caches HTTP and HTTPS responses to the filesystem so they may be reused, 66 * saving time and bandwidth. 67 * 68 * <h3>Cache Optimization</h3> 69 * To measure cache effectiveness, this class tracks three statistics: 70 * <ul> 71 * <li><strong>{@link #getRequestCount() Request Count:}</strong> the number 72 * of HTTP requests issued since this cache was created. 73 * <li><strong>{@link #getNetworkCount() Network Count:}</strong> the 74 * number of those requests that required network use. 75 * <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of 76 * those requests whose responses were served by the cache. 77 * </ul> 78 * Sometimes a request will result in a conditional cache hit. If the cache 79 * contains a stale copy of the response, the client will issue a conditional 80 * {@code GET}. The server will then send either the updated response if it has 81 * changed, or a short 'not modified' response if the client's copy is still 82 * valid. Such responses increment both the network count and hit count. 83 * 84 * <p>The best way to improve the cache hit rate is by configuring the web 85 * server to return cacheable responses. Although this client honors all <a 86 * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache 87 * headers, it doesn't cache partial responses. 88 * 89 * <h3>Force a Network Response</h3> 90 * In some situations, such as after a user clicks a 'refresh' button, it may be 91 * necessary to skip the cache, and fetch data directly from the server. To force 92 * a full refresh, add the {@code no-cache} directive: <pre> {@code 93 * connection.addRequestProperty("Cache-Control", "no-cache"); 94 * }</pre> 95 * If it is only necessary to force a cached response to be validated by the 96 * server, use the more efficient {@code max-age=0} instead: <pre> {@code 97 * connection.addRequestProperty("Cache-Control", "max-age=0"); 98 * }</pre> 99 * 100 * <h3>Force a Cache Response</h3> 101 * Sometimes you'll want to show resources if they are available immediately, 102 * but not otherwise. This can be used so your application can show 103 * <i>something</i> while waiting for the latest data to be downloaded. To 104 * restrict a request to locally-cached resources, add the {@code 105 * only-if-cached} directive: <pre> {@code 106 * try { 107 * connection.addRequestProperty("Cache-Control", "only-if-cached"); 108 * InputStream cached = connection.getInputStream(); 109 * // the resource was cached! show it 110 * } catch (FileNotFoundException e) { 111 * // the resource was not cached 112 * } 113 * }</pre> 114 * This technique works even better in situations where a stale response is 115 * better than no response. To permit stale cached responses, use the {@code 116 * max-stale} directive with the maximum staleness in seconds: <pre> {@code 117 * int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale 118 * connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale); 119 * }</pre> 120 */ 121public final class HttpResponseCache extends ResponseCache { 122 private static final char[] DIGITS = 123 { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; 124 125 // TODO: add APIs to iterate the cache? 126 private static final int VERSION = 201105; 127 private static final int ENTRY_METADATA = 0; 128 private static final int ENTRY_BODY = 1; 129 private static final int ENTRY_COUNT = 2; 130 131 private final DiskLruCache cache; 132 133 /* read and write statistics, all guarded by 'this' */ 134 private int writeSuccessCount; 135 private int writeAbortCount; 136 private int networkCount; 137 private int hitCount; 138 private int requestCount; 139 140 /** 141 * Although this class only exposes the limited ResponseCache API, it 142 * implements the full OkResponseCache interface. This field is used as a 143 * package private handle to the complete implementation. It delegates to 144 * public and private members of this type. 145 */ 146 final OkResponseCache okResponseCache = new OkResponseCache() { 147 @Override public CacheResponse get(URI uri, String requestMethod, 148 Map<String, List<String>> requestHeaders) throws IOException { 149 return HttpResponseCache.this.get(uri, requestMethod, requestHeaders); 150 } 151 152 @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { 153 return HttpResponseCache.this.put(uri, connection); 154 } 155 156 @Override public void maybeRemove(String requestMethod, URI uri) throws IOException { 157 HttpResponseCache.this.maybeRemove(requestMethod, uri); 158 } 159 160 @Override public void update( 161 CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException { 162 HttpResponseCache.this.update(conditionalCacheHit, connection); 163 } 164 165 @Override public void trackConditionalCacheHit() { 166 HttpResponseCache.this.trackConditionalCacheHit(); 167 } 168 169 @Override public void trackResponse(ResponseSource source) { 170 HttpResponseCache.this.trackResponse(source); 171 } 172 }; 173 174 public HttpResponseCache(File directory, long maxSize) throws IOException { 175 cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); 176 } 177 178 private String uriToKey(URI uri) { 179 try { 180 MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 181 byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8")); 182 return bytesToHexString(md5bytes); 183 } catch (NoSuchAlgorithmException e) { 184 throw new AssertionError(e); 185 } catch (UnsupportedEncodingException e) { 186 throw new AssertionError(e); 187 } 188 } 189 190 private static String bytesToHexString(byte[] bytes) { 191 char[] digits = DIGITS; 192 char[] buf = new char[bytes.length * 2]; 193 int c = 0; 194 for (byte b : bytes) { 195 buf[c++] = digits[(b >> 4) & 0xf]; 196 buf[c++] = digits[b & 0xf]; 197 } 198 return new String(buf); 199 } 200 201 @Override public CacheResponse get(URI uri, String requestMethod, 202 Map<String, List<String>> requestHeaders) { 203 String key = uriToKey(uri); 204 DiskLruCache.Snapshot snapshot; 205 Entry entry; 206 try { 207 snapshot = cache.get(key); 208 if (snapshot == null) { 209 return null; 210 } 211 entry = new Entry(snapshot.getInputStream(ENTRY_METADATA)); 212 } catch (IOException e) { 213 // Give up because the cache cannot be read. 214 return null; 215 } 216 217 if (!entry.matches(uri, requestMethod, requestHeaders)) { 218 snapshot.close(); 219 return null; 220 } 221 222 return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot) 223 : new EntryCacheResponse(entry, snapshot); 224 } 225 226 @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { 227 if (!(urlConnection instanceof HttpURLConnection)) { 228 return null; 229 } 230 231 HttpURLConnection httpConnection = (HttpURLConnection) urlConnection; 232 String requestMethod = httpConnection.getRequestMethod(); 233 234 if (maybeRemove(requestMethod, uri)) { 235 return null; 236 } 237 if (!requestMethod.equals("GET")) { 238 // Don't cache non-GET responses. We're technically allowed to cache 239 // HEAD requests and some POST requests, but the complexity of doing 240 // so is high and the benefit is low. 241 return null; 242 } 243 244 HttpEngine httpEngine = getHttpEngine(httpConnection); 245 if (httpEngine == null) { 246 // Don't cache unless the HTTP implementation is ours. 247 return null; 248 } 249 250 ResponseHeaders response = httpEngine.getResponseHeaders(); 251 if (response.hasVaryAll()) { 252 return null; 253 } 254 255 RawHeaders varyHeaders = 256 httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields()); 257 Entry entry = new Entry(uri, varyHeaders, httpConnection); 258 DiskLruCache.Editor editor = null; 259 try { 260 editor = cache.edit(uriToKey(uri)); 261 if (editor == null) { 262 return null; 263 } 264 entry.writeTo(editor); 265 return new CacheRequestImpl(editor); 266 } catch (IOException e) { 267 abortQuietly(editor); 268 return null; 269 } 270 } 271 272 /** 273 * Returns true if the supplied {@code requestMethod} potentially invalidates an entry in the 274 * cache. 275 */ 276 private boolean maybeRemove(String requestMethod, URI uri) { 277 if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals( 278 "DELETE")) { 279 try { 280 cache.remove(uriToKey(uri)); 281 } catch (IOException ignored) { 282 // The cache cannot be written. 283 } 284 return true; 285 } 286 return false; 287 } 288 289 private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection) 290 throws IOException { 291 HttpEngine httpEngine = getHttpEngine(httpConnection); 292 URI uri = httpEngine.getUri(); 293 ResponseHeaders response = httpEngine.getResponseHeaders(); 294 RawHeaders varyHeaders = 295 httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields()); 296 Entry entry = new Entry(uri, varyHeaders, httpConnection); 297 DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse) 298 ? ((EntryCacheResponse) conditionalCacheHit).snapshot 299 : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot; 300 DiskLruCache.Editor editor = null; 301 try { 302 editor = snapshot.edit(); // returns null if snapshot is not current 303 if (editor != null) { 304 entry.writeTo(editor); 305 editor.commit(); 306 } 307 } catch (IOException e) { 308 abortQuietly(editor); 309 } 310 } 311 312 private void abortQuietly(DiskLruCache.Editor editor) { 313 // Give up because the cache cannot be written. 314 try { 315 if (editor != null) { 316 editor.abort(); 317 } 318 } catch (IOException ignored) { 319 } 320 } 321 322 private HttpEngine getHttpEngine(URLConnection httpConnection) { 323 if (httpConnection instanceof HttpURLConnectionImpl) { 324 return ((HttpURLConnectionImpl) httpConnection).getHttpEngine(); 325 } else if (httpConnection instanceof HttpsURLConnectionImpl) { 326 return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine(); 327 } else { 328 return null; 329 } 330 } 331 332 /** 333 * Closes the cache and deletes all of its stored values. This will delete 334 * all files in the cache directory including files that weren't created by 335 * the cache. 336 */ 337 public void delete() throws IOException { 338 cache.delete(); 339 } 340 341 public synchronized int getWriteAbortCount() { 342 return writeAbortCount; 343 } 344 345 public synchronized int getWriteSuccessCount() { 346 return writeSuccessCount; 347 } 348 349 public long getSize() { 350 return cache.size(); 351 } 352 353 public long getMaxSize() { 354 return cache.getMaxSize(); 355 } 356 357 public void flush() throws IOException { 358 cache.flush(); 359 } 360 361 public void close() throws IOException { 362 cache.close(); 363 } 364 365 public File getDirectory() { 366 return cache.getDirectory(); 367 } 368 369 public boolean isClosed() { 370 return cache.isClosed(); 371 } 372 373 private synchronized void trackResponse(ResponseSource source) { 374 requestCount++; 375 376 switch (source) { 377 case CACHE: 378 hitCount++; 379 break; 380 case CONDITIONAL_CACHE: 381 case NETWORK: 382 networkCount++; 383 break; 384 } 385 } 386 387 private synchronized void trackConditionalCacheHit() { 388 hitCount++; 389 } 390 391 public synchronized int getNetworkCount() { 392 return networkCount; 393 } 394 395 public synchronized int getHitCount() { 396 return hitCount; 397 } 398 399 public synchronized int getRequestCount() { 400 return requestCount; 401 } 402 403 private final class CacheRequestImpl extends CacheRequest { 404 private final DiskLruCache.Editor editor; 405 private OutputStream cacheOut; 406 private boolean done; 407 private OutputStream body; 408 409 public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { 410 this.editor = editor; 411 this.cacheOut = editor.newOutputStream(ENTRY_BODY); 412 this.body = new FilterOutputStream(cacheOut) { 413 @Override public void close() throws IOException { 414 synchronized (HttpResponseCache.this) { 415 if (done) { 416 return; 417 } 418 done = true; 419 writeSuccessCount++; 420 } 421 super.close(); 422 editor.commit(); 423 } 424 425 @Override public void write(byte[] buffer, int offset, int length) throws IOException { 426 // Since we don't override "write(int oneByte)", we can write directly to "out" 427 // and avoid the inefficient implementation from the FilterOutputStream. 428 out.write(buffer, offset, length); 429 } 430 }; 431 } 432 433 @Override public void abort() { 434 synchronized (HttpResponseCache.this) { 435 if (done) { 436 return; 437 } 438 done = true; 439 writeAbortCount++; 440 } 441 Util.closeQuietly(cacheOut); 442 try { 443 editor.abort(); 444 } catch (IOException ignored) { 445 } 446 } 447 448 @Override public OutputStream getBody() throws IOException { 449 return body; 450 } 451 } 452 453 private static final class Entry { 454 private final String uri; 455 private final RawHeaders varyHeaders; 456 private final String requestMethod; 457 private final RawHeaders responseHeaders; 458 private final String cipherSuite; 459 private final Certificate[] peerCertificates; 460 private final Certificate[] localCertificates; 461 462 /** 463 * Reads an entry from an input stream. A typical entry looks like this: 464 * <pre>{@code 465 * http://google.com/foo 466 * GET 467 * 2 468 * Accept-Language: fr-CA 469 * Accept-Charset: UTF-8 470 * HTTP/1.1 200 OK 471 * 3 472 * Content-Type: image/png 473 * Content-Length: 100 474 * Cache-Control: max-age=600 475 * }</pre> 476 * 477 * <p>A typical HTTPS file looks like this: 478 * <pre>{@code 479 * https://google.com/foo 480 * GET 481 * 2 482 * Accept-Language: fr-CA 483 * Accept-Charset: UTF-8 484 * HTTP/1.1 200 OK 485 * 3 486 * Content-Type: image/png 487 * Content-Length: 100 488 * Cache-Control: max-age=600 489 * 490 * AES_256_WITH_MD5 491 * 2 492 * base64-encoded peerCertificate[0] 493 * base64-encoded peerCertificate[1] 494 * -1 495 * }</pre> 496 * The file is newline separated. The first two lines are the URL and 497 * the request method. Next is the number of HTTP Vary request header 498 * lines, followed by those lines. 499 * 500 * <p>Next is the response status line, followed by the number of HTTP 501 * response header lines, followed by those lines. 502 * 503 * <p>HTTPS responses also contain SSL session information. This begins 504 * with a blank line, and then a line containing the cipher suite. Next 505 * is the length of the peer certificate chain. These certificates are 506 * base64-encoded and appear each on their own line. The next line 507 * contains the length of the local certificate chain. These 508 * certificates are also base64-encoded and appear each on their own 509 * line. A length of -1 is used to encode a null array. 510 */ 511 public Entry(InputStream in) throws IOException { 512 try { 513 StrictLineReader reader = new StrictLineReader(in, US_ASCII); 514 uri = reader.readLine(); 515 requestMethod = reader.readLine(); 516 varyHeaders = new RawHeaders(); 517 int varyRequestHeaderLineCount = reader.readInt(); 518 for (int i = 0; i < varyRequestHeaderLineCount; i++) { 519 varyHeaders.addLine(reader.readLine()); 520 } 521 522 responseHeaders = new RawHeaders(); 523 responseHeaders.setStatusLine(reader.readLine()); 524 int responseHeaderLineCount = reader.readInt(); 525 for (int i = 0; i < responseHeaderLineCount; i++) { 526 responseHeaders.addLine(reader.readLine()); 527 } 528 529 if (isHttps()) { 530 String blank = reader.readLine(); 531 if (blank.length() > 0) { 532 throw new IOException("expected \"\" but was \"" + blank + "\""); 533 } 534 cipherSuite = reader.readLine(); 535 peerCertificates = readCertArray(reader); 536 localCertificates = readCertArray(reader); 537 } else { 538 cipherSuite = null; 539 peerCertificates = null; 540 localCertificates = null; 541 } 542 } finally { 543 in.close(); 544 } 545 } 546 547 public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection) 548 throws IOException { 549 this.uri = uri.toString(); 550 this.varyHeaders = varyHeaders; 551 this.requestMethod = httpConnection.getRequestMethod(); 552 this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true); 553 554 SSLSocket sslSocket = getSslSocket(httpConnection); 555 if (sslSocket != null) { 556 cipherSuite = sslSocket.getSession().getCipherSuite(); 557 Certificate[] peerCertificatesNonFinal = null; 558 try { 559 peerCertificatesNonFinal = sslSocket.getSession().getPeerCertificates(); 560 } catch (SSLPeerUnverifiedException ignored) { 561 } 562 peerCertificates = peerCertificatesNonFinal; 563 localCertificates = sslSocket.getSession().getLocalCertificates(); 564 } else { 565 cipherSuite = null; 566 peerCertificates = null; 567 localCertificates = null; 568 } 569 } 570 571 /** 572 * Returns the SSL socket used by {@code httpConnection} for HTTPS, nor null 573 * if the connection isn't using HTTPS. Since we permit redirects across 574 * protocols (HTTP to HTTPS or vice versa), the implementation type of the 575 * connection doesn't necessarily match the implementation type of its HTTP 576 * engine. 577 */ 578 private SSLSocket getSslSocket(HttpURLConnection httpConnection) { 579 HttpEngine engine = httpConnection instanceof HttpsURLConnectionImpl 580 ? ((HttpsURLConnectionImpl) httpConnection).getHttpEngine() 581 : ((HttpURLConnectionImpl) httpConnection).getHttpEngine(); 582 return engine instanceof HttpsEngine 583 ? ((HttpsEngine) engine).getSslSocket() 584 : null; 585 } 586 587 public void writeTo(DiskLruCache.Editor editor) throws IOException { 588 OutputStream out = editor.newOutputStream(ENTRY_METADATA); 589 Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8)); 590 591 writer.write(uri + '\n'); 592 writer.write(requestMethod + '\n'); 593 writer.write(Integer.toString(varyHeaders.length()) + '\n'); 594 for (int i = 0; i < varyHeaders.length(); i++) { 595 writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n'); 596 } 597 598 writer.write(responseHeaders.getStatusLine() + '\n'); 599 writer.write(Integer.toString(responseHeaders.length()) + '\n'); 600 for (int i = 0; i < responseHeaders.length(); i++) { 601 writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n'); 602 } 603 604 if (isHttps()) { 605 writer.write('\n'); 606 writer.write(cipherSuite + '\n'); 607 writeCertArray(writer, peerCertificates); 608 writeCertArray(writer, localCertificates); 609 } 610 writer.close(); 611 } 612 613 private boolean isHttps() { 614 return uri.startsWith("https://"); 615 } 616 617 private Certificate[] readCertArray(StrictLineReader reader) throws IOException { 618 int length = reader.readInt(); 619 if (length == -1) { 620 return null; 621 } 622 try { 623 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 624 Certificate[] result = new Certificate[length]; 625 for (int i = 0; i < result.length; i++) { 626 String line = reader.readLine(); 627 byte[] bytes = Base64.decode(line.getBytes("US-ASCII")); 628 result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes)); 629 } 630 return result; 631 } catch (CertificateException e) { 632 throw new IOException(e.getMessage()); 633 } 634 } 635 636 private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException { 637 if (certificates == null) { 638 writer.write("-1\n"); 639 return; 640 } 641 try { 642 writer.write(Integer.toString(certificates.length) + '\n'); 643 for (Certificate certificate : certificates) { 644 byte[] bytes = certificate.getEncoded(); 645 String line = Base64.encode(bytes); 646 writer.write(line + '\n'); 647 } 648 } catch (CertificateEncodingException e) { 649 throw new IOException(e.getMessage()); 650 } 651 } 652 653 public boolean matches(URI uri, String requestMethod, 654 Map<String, List<String>> requestHeaders) { 655 return this.uri.equals(uri.toString()) 656 && this.requestMethod.equals(requestMethod) 657 && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false), 658 requestHeaders); 659 } 660 } 661 662 /** 663 * Returns an input stream that reads the body of a snapshot, closing the 664 * snapshot when the stream is closed. 665 */ 666 private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) { 667 return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) { 668 @Override public void close() throws IOException { 669 snapshot.close(); 670 super.close(); 671 } 672 }; 673 } 674 675 static class EntryCacheResponse extends CacheResponse { 676 private final Entry entry; 677 private final DiskLruCache.Snapshot snapshot; 678 private final InputStream in; 679 680 public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { 681 this.entry = entry; 682 this.snapshot = snapshot; 683 this.in = newBodyInputStream(snapshot); 684 } 685 686 @Override public Map<String, List<String>> getHeaders() { 687 return entry.responseHeaders.toMultimap(true); 688 } 689 690 @Override public InputStream getBody() { 691 return in; 692 } 693 } 694 695 static class EntrySecureCacheResponse extends SecureCacheResponse { 696 private final Entry entry; 697 private final DiskLruCache.Snapshot snapshot; 698 private final InputStream in; 699 700 public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { 701 this.entry = entry; 702 this.snapshot = snapshot; 703 this.in = newBodyInputStream(snapshot); 704 } 705 706 @Override public Map<String, List<String>> getHeaders() { 707 return entry.responseHeaders.toMultimap(true); 708 } 709 710 @Override public InputStream getBody() { 711 return in; 712 } 713 714 @Override public String getCipherSuite() { 715 return entry.cipherSuite; 716 } 717 718 @Override public List<Certificate> getServerCertificateChain() 719 throws SSLPeerUnverifiedException { 720 if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { 721 throw new SSLPeerUnverifiedException(null); 722 } 723 return Arrays.asList(entry.peerCertificates.clone()); 724 } 725 726 @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { 727 if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { 728 throw new SSLPeerUnverifiedException(null); 729 } 730 return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal(); 731 } 732 733 @Override public List<Certificate> getLocalCertificateChain() { 734 if (entry.localCertificates == null || entry.localCertificates.length == 0) { 735 return null; 736 } 737 return Arrays.asList(entry.localCertificates.clone()); 738 } 739 740 @Override public Principal getLocalPrincipal() { 741 if (entry.localCertificates == null || entry.localCertificates.length == 0) { 742 return null; 743 } 744 return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal(); 745 } 746 } 747} 748