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