Home | History | Annotate | Download | only in okhttp
      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.Base64;
     20 import com.squareup.okhttp.internal.DiskLruCache;
     21 import com.squareup.okhttp.internal.StrictLineReader;
     22 import com.squareup.okhttp.internal.Util;
     23 import com.squareup.okhttp.internal.http.HttpEngine;
     24 import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
     25 import com.squareup.okhttp.internal.http.HttpsEngine;
     26 import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
     27 import com.squareup.okhttp.internal.http.RawHeaders;
     28 import com.squareup.okhttp.internal.http.ResponseHeaders;
     29 import java.io.BufferedWriter;
     30 import java.io.ByteArrayInputStream;
     31 import java.io.File;
     32 import java.io.FilterInputStream;
     33 import java.io.FilterOutputStream;
     34 import java.io.IOException;
     35 import java.io.InputStream;
     36 import java.io.OutputStream;
     37 import java.io.OutputStreamWriter;
     38 import java.io.UnsupportedEncodingException;
     39 import java.io.Writer;
     40 import java.net.CacheRequest;
     41 import java.net.CacheResponse;
     42 import java.net.HttpURLConnection;
     43 import java.net.ResponseCache;
     44 import java.net.SecureCacheResponse;
     45 import java.net.URI;
     46 import java.net.URLConnection;
     47 import java.security.MessageDigest;
     48 import java.security.NoSuchAlgorithmException;
     49 import java.security.Principal;
     50 import java.security.cert.Certificate;
     51 import java.security.cert.CertificateEncodingException;
     52 import java.security.cert.CertificateException;
     53 import java.security.cert.CertificateFactory;
     54 import java.security.cert.X509Certificate;
     55 import java.util.Arrays;
     56 import java.util.List;
     57 import java.util.Map;
     58 import javax.net.ssl.SSLPeerUnverifiedException;
     59 import javax.net.ssl.SSLSocket;
     60 
     61 import static com.squareup.okhttp.internal.Util.US_ASCII;
     62 import 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  */
    121 public 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