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