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