• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.internal.http;
18 
19 import com.squareup.okhttp.OkResponseCache;
20 import com.squareup.okhttp.ResponseSource;
21 import com.squareup.okhttp.internal.Base64;
22 import com.squareup.okhttp.internal.DiskLruCache;
23 import com.squareup.okhttp.internal.StrictLineReader;
24 import com.squareup.okhttp.internal.Util;
25 import java.io.BufferedWriter;
26 import java.io.ByteArrayInputStream;
27 import java.io.File;
28 import java.io.FilterInputStream;
29 import java.io.FilterOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.io.OutputStreamWriter;
34 import java.io.UnsupportedEncodingException;
35 import java.io.Writer;
36 import java.net.CacheRequest;
37 import java.net.CacheResponse;
38 import java.net.HttpURLConnection;
39 import java.net.ResponseCache;
40 import java.net.SecureCacheResponse;
41 import java.net.URI;
42 import java.net.URLConnection;
43 import java.security.MessageDigest;
44 import java.security.NoSuchAlgorithmException;
45 import java.security.Principal;
46 import java.security.cert.Certificate;
47 import java.security.cert.CertificateEncodingException;
48 import java.security.cert.CertificateException;
49 import java.security.cert.CertificateFactory;
50 import java.security.cert.X509Certificate;
51 import java.util.Arrays;
52 import java.util.List;
53 import java.util.Map;
54 import javax.net.ssl.HttpsURLConnection;
55 import javax.net.ssl.SSLPeerUnverifiedException;
56 
57 import static com.squareup.okhttp.internal.Util.US_ASCII;
58 import static com.squareup.okhttp.internal.Util.UTF_8;
59 
60 /**
61  * Cache responses in a directory on the file system. Most clients should use
62  * {@code android.net.HttpResponseCache}, the stable, documented front end for
63  * this.
64  */
65 public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
66   private static final char[] DIGITS =
67       { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
68 
69   // TODO: add APIs to iterate the cache?
70   private static final int VERSION = 201105;
71   private static final int ENTRY_METADATA = 0;
72   private static final int ENTRY_BODY = 1;
73   private static final int ENTRY_COUNT = 2;
74 
75   private final DiskLruCache cache;
76 
77   /* read and write statistics, all guarded by 'this' */
78   private int writeSuccessCount;
79   private int writeAbortCount;
80   private int networkCount;
81   private int hitCount;
82   private int requestCount;
83 
HttpResponseCache(File directory, long maxSize)84   public HttpResponseCache(File directory, long maxSize) throws IOException {
85     cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
86   }
87 
uriToKey(URI uri)88   private String uriToKey(URI uri) {
89     try {
90       MessageDigest messageDigest = MessageDigest.getInstance("MD5");
91       byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
92       return bytesToHexString(md5bytes);
93     } catch (NoSuchAlgorithmException e) {
94       throw new AssertionError(e);
95     } catch (UnsupportedEncodingException e) {
96       throw new AssertionError(e);
97     }
98   }
99 
bytesToHexString(byte[] bytes)100   private static String bytesToHexString(byte[] bytes) {
101     char[] digits = DIGITS;
102     char[] buf = new char[bytes.length * 2];
103     int c = 0;
104     for (byte b : bytes) {
105       buf[c++] = digits[(b >> 4) & 0xf];
106       buf[c++] = digits[b & 0xf];
107     }
108     return new String(buf);
109   }
110 
get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)111   @Override public CacheResponse get(URI uri, String requestMethod,
112       Map<String, List<String>> requestHeaders) {
113     String key = uriToKey(uri);
114     DiskLruCache.Snapshot snapshot;
115     Entry entry;
116     try {
117       snapshot = cache.get(key);
118       if (snapshot == null) {
119         return null;
120       }
121       entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
122     } catch (IOException e) {
123       // Give up because the cache cannot be read.
124       return null;
125     }
126 
127     if (!entry.matches(uri, requestMethod, requestHeaders)) {
128       snapshot.close();
129       return null;
130     }
131 
132     return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
133         : new EntryCacheResponse(entry, snapshot);
134   }
135 
put(URI uri, URLConnection urlConnection)136   @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
137     if (!(urlConnection instanceof HttpURLConnection)) {
138       return null;
139     }
140 
141     HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
142     String requestMethod = httpConnection.getRequestMethod();
143     String key = uriToKey(uri);
144 
145     if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
146         "DELETE")) {
147       try {
148         cache.remove(key);
149       } catch (IOException ignored) {
150         // The cache cannot be written.
151       }
152       return null;
153     } else if (!requestMethod.equals("GET")) {
154       // Don't cache non-GET responses. We're technically allowed to cache
155       // HEAD requests and some POST requests, but the complexity of doing
156       // so is high and the benefit is low.
157       return null;
158     }
159 
160     HttpEngine httpEngine = getHttpEngine(httpConnection);
161     if (httpEngine == null) {
162       // Don't cache unless the HTTP implementation is ours.
163       return null;
164     }
165 
166     ResponseHeaders response = httpEngine.getResponseHeaders();
167     if (response.hasVaryAll()) {
168       return null;
169     }
170 
171     RawHeaders varyHeaders =
172         httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
173     Entry entry = new Entry(uri, varyHeaders, httpConnection);
174     DiskLruCache.Editor editor = null;
175     try {
176       editor = cache.edit(key);
177       if (editor == null) {
178         return null;
179       }
180       entry.writeTo(editor);
181       return new CacheRequestImpl(editor);
182     } catch (IOException e) {
183       abortQuietly(editor);
184       return null;
185     }
186   }
187 
188   /**
189    * Handles a conditional request hit by updating the stored cache response
190    * with the headers from {@code httpConnection}. The cached response body is
191    * not updated. If the stored response has changed since {@code
192    * conditionalCacheHit} was returned, this does nothing.
193    */
update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)194   @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
195       throws IOException {
196     HttpEngine httpEngine = getHttpEngine(httpConnection);
197     URI uri = httpEngine.getUri();
198     ResponseHeaders response = httpEngine.getResponseHeaders();
199     RawHeaders varyHeaders =
200         httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
201     Entry entry = new Entry(uri, varyHeaders, httpConnection);
202     DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
203         ? ((EntryCacheResponse) conditionalCacheHit).snapshot
204         : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
205     DiskLruCache.Editor editor = null;
206     try {
207       editor = snapshot.edit(); // returns null if snapshot is not current
208       if (editor != null) {
209         entry.writeTo(editor);
210         editor.commit();
211       }
212     } catch (IOException e) {
213       abortQuietly(editor);
214     }
215   }
216 
abortQuietly(DiskLruCache.Editor editor)217   private void abortQuietly(DiskLruCache.Editor editor) {
218     // Give up because the cache cannot be written.
219     try {
220       if (editor != null) {
221         editor.abort();
222       }
223     } catch (IOException ignored) {
224     }
225   }
226 
getHttpEngine(URLConnection httpConnection)227   private HttpEngine getHttpEngine(URLConnection httpConnection) {
228     if (httpConnection instanceof HttpURLConnectionImpl) {
229       return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
230     } else if (httpConnection instanceof HttpsURLConnectionImpl) {
231       return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
232     } else {
233       return null;
234     }
235   }
236 
getCache()237   public DiskLruCache getCache() {
238     return cache;
239   }
240 
getWriteAbortCount()241   public synchronized int getWriteAbortCount() {
242     return writeAbortCount;
243   }
244 
getWriteSuccessCount()245   public synchronized int getWriteSuccessCount() {
246     return writeSuccessCount;
247   }
248 
trackResponse(ResponseSource source)249   public synchronized void trackResponse(ResponseSource source) {
250     requestCount++;
251 
252     switch (source) {
253       case CACHE:
254         hitCount++;
255         break;
256       case CONDITIONAL_CACHE:
257       case NETWORK:
258         networkCount++;
259         break;
260     }
261   }
262 
trackConditionalCacheHit()263   public synchronized void trackConditionalCacheHit() {
264     hitCount++;
265   }
266 
getNetworkCount()267   public synchronized int getNetworkCount() {
268     return networkCount;
269   }
270 
getHitCount()271   public synchronized int getHitCount() {
272     return hitCount;
273   }
274 
getRequestCount()275   public synchronized int getRequestCount() {
276     return requestCount;
277   }
278 
279   private final class CacheRequestImpl extends CacheRequest {
280     private final DiskLruCache.Editor editor;
281     private OutputStream cacheOut;
282     private boolean done;
283     private OutputStream body;
284 
CacheRequestImpl(final DiskLruCache.Editor editor)285     public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
286       this.editor = editor;
287       this.cacheOut = editor.newOutputStream(ENTRY_BODY);
288       this.body = new FilterOutputStream(cacheOut) {
289         @Override public void close() throws IOException {
290           synchronized (HttpResponseCache.this) {
291             if (done) {
292               return;
293             }
294             done = true;
295             writeSuccessCount++;
296           }
297           super.close();
298           editor.commit();
299         }
300 
301         @Override
302         public void write(byte[] buffer, int offset, int length) throws IOException {
303           // Since we don't override "write(int oneByte)", we can write directly to "out"
304           // and avoid the inefficient implementation from the FilterOutputStream.
305           out.write(buffer, offset, length);
306         }
307       };
308     }
309 
abort()310     @Override public void abort() {
311       synchronized (HttpResponseCache.this) {
312         if (done) {
313           return;
314         }
315         done = true;
316         writeAbortCount++;
317       }
318       Util.closeQuietly(cacheOut);
319       try {
320         editor.abort();
321       } catch (IOException ignored) {
322       }
323     }
324 
getBody()325     @Override public OutputStream getBody() throws IOException {
326       return body;
327     }
328   }
329 
330   private static final class Entry {
331     private final String uri;
332     private final RawHeaders varyHeaders;
333     private final String requestMethod;
334     private final RawHeaders responseHeaders;
335     private final String cipherSuite;
336     private final Certificate[] peerCertificates;
337     private final Certificate[] localCertificates;
338 
339     /**
340      * Reads an entry from an input stream. A typical entry looks like this:
341      * <pre>{@code
342      *   http://google.com/foo
343      *   GET
344      *   2
345      *   Accept-Language: fr-CA
346      *   Accept-Charset: UTF-8
347      *   HTTP/1.1 200 OK
348      *   3
349      *   Content-Type: image/png
350      *   Content-Length: 100
351      *   Cache-Control: max-age=600
352      * }</pre>
353      *
354      * <p>A typical HTTPS file looks like this:
355      * <pre>{@code
356      *   https://google.com/foo
357      *   GET
358      *   2
359      *   Accept-Language: fr-CA
360      *   Accept-Charset: UTF-8
361      *   HTTP/1.1 200 OK
362      *   3
363      *   Content-Type: image/png
364      *   Content-Length: 100
365      *   Cache-Control: max-age=600
366      *
367      *   AES_256_WITH_MD5
368      *   2
369      *   base64-encoded peerCertificate[0]
370      *   base64-encoded peerCertificate[1]
371      *   -1
372      * }</pre>
373      * The file is newline separated. The first two lines are the URL and
374      * the request method. Next is the number of HTTP Vary request header
375      * lines, followed by those lines.
376      *
377      * <p>Next is the response status line, followed by the number of HTTP
378      * response header lines, followed by those lines.
379      *
380      * <p>HTTPS responses also contain SSL session information. This begins
381      * with a blank line, and then a line containing the cipher suite. Next
382      * is the length of the peer certificate chain. These certificates are
383      * base64-encoded and appear each on their own line. The next line
384      * contains the length of the local certificate chain. These
385      * certificates are also base64-encoded and appear each on their own
386      * line. A length of -1 is used to encode a null array.
387      */
Entry(InputStream in)388     public Entry(InputStream in) throws IOException {
389       try {
390         StrictLineReader reader = new StrictLineReader(in, US_ASCII);
391         uri = reader.readLine();
392         requestMethod = reader.readLine();
393         varyHeaders = new RawHeaders();
394         int varyRequestHeaderLineCount = reader.readInt();
395         for (int i = 0; i < varyRequestHeaderLineCount; i++) {
396           varyHeaders.addLine(reader.readLine());
397         }
398 
399         responseHeaders = new RawHeaders();
400         responseHeaders.setStatusLine(reader.readLine());
401         int responseHeaderLineCount = reader.readInt();
402         for (int i = 0; i < responseHeaderLineCount; i++) {
403           responseHeaders.addLine(reader.readLine());
404         }
405 
406         if (isHttps()) {
407           String blank = reader.readLine();
408           if (!blank.isEmpty()) {
409             throw new IOException("expected \"\" but was \"" + blank + "\"");
410           }
411           cipherSuite = reader.readLine();
412           peerCertificates = readCertArray(reader);
413           localCertificates = readCertArray(reader);
414         } else {
415           cipherSuite = null;
416           peerCertificates = null;
417           localCertificates = null;
418         }
419       } finally {
420         in.close();
421       }
422     }
423 
Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)424     public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
425         throws IOException {
426       this.uri = uri.toString();
427       this.varyHeaders = varyHeaders;
428       this.requestMethod = httpConnection.getRequestMethod();
429       this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
430 
431       if (isHttps()) {
432         HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
433         cipherSuite = httpsConnection.getCipherSuite();
434         Certificate[] peerCertificatesNonFinal = null;
435         try {
436           peerCertificatesNonFinal = httpsConnection.getServerCertificates();
437         } catch (SSLPeerUnverifiedException ignored) {
438         }
439         peerCertificates = peerCertificatesNonFinal;
440         localCertificates = httpsConnection.getLocalCertificates();
441       } else {
442         cipherSuite = null;
443         peerCertificates = null;
444         localCertificates = null;
445       }
446     }
447 
writeTo(DiskLruCache.Editor editor)448     public void writeTo(DiskLruCache.Editor editor) throws IOException {
449       OutputStream out = editor.newOutputStream(ENTRY_METADATA);
450       Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
451 
452       writer.write(uri + '\n');
453       writer.write(requestMethod + '\n');
454       writer.write(Integer.toString(varyHeaders.length()) + '\n');
455       for (int i = 0; i < varyHeaders.length(); i++) {
456         writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
457       }
458 
459       writer.write(responseHeaders.getStatusLine() + '\n');
460       writer.write(Integer.toString(responseHeaders.length()) + '\n');
461       for (int i = 0; i < responseHeaders.length(); i++) {
462         writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
463       }
464 
465       if (isHttps()) {
466         writer.write('\n');
467         writer.write(cipherSuite + '\n');
468         writeCertArray(writer, peerCertificates);
469         writeCertArray(writer, localCertificates);
470       }
471       writer.close();
472     }
473 
isHttps()474     private boolean isHttps() {
475       return uri.startsWith("https://");
476     }
477 
readCertArray(StrictLineReader reader)478     private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
479       int length = reader.readInt();
480       if (length == -1) {
481         return null;
482       }
483       try {
484         CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
485         Certificate[] result = new Certificate[length];
486         for (int i = 0; i < result.length; i++) {
487           String line = reader.readLine();
488           byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
489           result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
490         }
491         return result;
492       } catch (CertificateException e) {
493         throw new IOException(e);
494       }
495     }
496 
writeCertArray(Writer writer, Certificate[] certificates)497     private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
498       if (certificates == null) {
499         writer.write("-1\n");
500         return;
501       }
502       try {
503         writer.write(Integer.toString(certificates.length) + '\n');
504         for (Certificate certificate : certificates) {
505           byte[] bytes = certificate.getEncoded();
506           String line = Base64.encode(bytes);
507           writer.write(line + '\n');
508         }
509       } catch (CertificateEncodingException e) {
510         throw new IOException(e);
511       }
512     }
513 
matches(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)514     public boolean matches(URI uri, String requestMethod,
515         Map<String, List<String>> requestHeaders) {
516       return this.uri.equals(uri.toString())
517           && this.requestMethod.equals(requestMethod)
518           && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
519           requestHeaders);
520     }
521   }
522 
523   /**
524    * Returns an input stream that reads the body of a snapshot, closing the
525    * snapshot when the stream is closed.
526    */
newBodyInputStream(final DiskLruCache.Snapshot snapshot)527   private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
528     return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
529       @Override public void close() throws IOException {
530         snapshot.close();
531         super.close();
532       }
533     };
534   }
535 
536   static class EntryCacheResponse extends CacheResponse {
537     private final Entry entry;
538     private final DiskLruCache.Snapshot snapshot;
539     private final InputStream in;
540 
541     public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
542       this.entry = entry;
543       this.snapshot = snapshot;
544       this.in = newBodyInputStream(snapshot);
545     }
546 
547     @Override public Map<String, List<String>> getHeaders() {
548       return entry.responseHeaders.toMultimap(true);
549     }
550 
551     @Override public InputStream getBody() {
552       return in;
553     }
554   }
555 
556   static class EntrySecureCacheResponse extends SecureCacheResponse {
557     private final Entry entry;
558     private final DiskLruCache.Snapshot snapshot;
559     private final InputStream in;
560 
561     public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
562       this.entry = entry;
563       this.snapshot = snapshot;
564       this.in = newBodyInputStream(snapshot);
565     }
566 
567     @Override public Map<String, List<String>> getHeaders() {
568       return entry.responseHeaders.toMultimap(true);
569     }
570 
571     @Override public InputStream getBody() {
572       return in;
573     }
574 
575     @Override public String getCipherSuite() {
576       return entry.cipherSuite;
577     }
578 
579     @Override public List<Certificate> getServerCertificateChain()
580         throws SSLPeerUnverifiedException {
581       if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
582         throw new SSLPeerUnverifiedException(null);
583       }
584       return Arrays.asList(entry.peerCertificates.clone());
585     }
586 
587     @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
588       if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
589         throw new SSLPeerUnverifiedException(null);
590       }
591       return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
592     }
593 
594     @Override public List<Certificate> getLocalCertificateChain() {
595       if (entry.localCertificates == null || entry.localCertificates.length == 0) {
596         return null;
597       }
598       return Arrays.asList(entry.localCertificates.clone());
599     }
600 
601     @Override public Principal getLocalPrincipal() {
602       if (entry.localCertificates == null || entry.localCertificates.length == 0) {
603         return null;
604       }
605       return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
606     }
607   }
608 }
609