summaryrefslogtreecommitdiffstats
path: root/support
diff options
context:
space:
mode:
authorJesse Wilson <jessewilson@google.com>2011-05-10 15:28:04 -0700
committerJesse Wilson <jessewilson@google.com>2011-05-10 16:39:48 -0700
commit433e3fac172d0c4449051b0c61c0c63b298a0903 (patch)
tree7fcb9e35969e9739939315350ece045809a73ef6 /support
parent8e0bdcf8bb815078e28679b8111d590d2da83328 (diff)
downloadlibcore-433e3fac172d0c4449051b0c61c0c63b298a0903.zip
libcore-433e3fac172d0c4449051b0c61c0c63b298a0903.tar.gz
libcore-433e3fac172d0c4449051b0c61c0c63b298a0903.tar.bz2
Adopt DiskLruCache in HttpResponseCache.
http://b/3180373 Change-Id: I55ed45c5d0b8fc72cf50e8912416367ac802dc7a
Diffstat (limited to 'support')
-rw-r--r--support/src/test/java/libcore/net/http/HttpResponseCache.java440
1 files changed, 296 insertions, 144 deletions
diff --git a/support/src/test/java/libcore/net/http/HttpResponseCache.java b/support/src/test/java/libcore/net/http/HttpResponseCache.java
index a551e27..516d4ab 100644
--- a/support/src/test/java/libcore/net/http/HttpResponseCache.java
+++ b/support/src/test/java/libcore/net/http/HttpResponseCache.java
@@ -16,11 +16,17 @@
package libcore.net.http;
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
@@ -28,16 +34,23 @@ import java.net.ResponseCache;
import java.net.SecureCacheResponse;
import java.net.URI;
import java.net.URLConnection;
+import java.nio.charset.Charsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.Certificate;
-import java.util.ArrayList;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.util.Arrays;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
+import libcore.io.Base64;
+import libcore.io.DiskLruCache;
+import libcore.io.IoUtils;
/**
* Cache all responses in memory by URI.
@@ -45,29 +58,49 @@ import javax.net.ssl.SSLPeerUnverifiedException;
* TODO: disk storage, tuning knobs, LRU
* TODO: move this class to android.util
*/
-public final class HttpResponseCache extends ResponseCache {
- private final Map<URI, Entry> entries = new HashMap<URI, Entry>();
+public final class HttpResponseCache extends ResponseCache implements Closeable {
+ private final DiskLruCache cache;
private int abortCount;
private int successCount;
private int hitCount;
private int missCount;
+ public HttpResponseCache(File directory, int maxSize) throws IOException {
+ cache = DiskLruCache.open(directory, 2, maxSize);
+ }
+
+ private String uriToKey(URI uri) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+ byte[] md5bytes = messageDigest.digest(uri.toString().getBytes(Charsets.UTF_8));
+ return IntegralToString.bytesToHexString(md5bytes, false);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError();
+ }
+ }
+
@Override public synchronized CacheResponse get(URI uri, String requestMethod,
Map<String, List<String>> requestHeaders) throws IOException {
- Entry entry = entries.get(uri);
- if (entry == null) {
+ String key = uriToKey(uri);
+ DiskLruCache.Snapshot snapshot = cache.read(key);
+
+ if (snapshot == null) {
missCount++;
return null;
}
- if (!requestMethod.equals(entry.requestMethod)) {
+ Entry entry = new Entry(new BufferedInputStream(snapshot.getInputStream(0)));
+ if (!entry.matches(uri, requestMethod)) {
+ snapshot.close();
+ missCount++;
return null;
}
- // RawHeaders headers = RawHeaders.fromMultimap(entry.responseHeaders);
-
hitCount++;
- return entry.asResponse();
+ InputStream body = snapshot.getInputStream(1);
+ return entry.isHttps()
+ ? entry.newSecureCacheResponse(body)
+ : entry.newCacheResponse(body);
}
@Override public CacheRequest put(URI uri, URLConnection urlConnection)
@@ -78,11 +111,13 @@ public final class HttpResponseCache extends ResponseCache {
HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
String requestMethod = httpConnection.getRequestMethod();
+ String key = uriToKey(uri);
// Invalidate the cache on POST, PUT and DELETE.
- if (requestMethod.equals("POST") || requestMethod.equals("PUT")
- || requestMethod.equals("DELETE")) {
- entries.remove(uri);
+ if (requestMethod.equals(HttpEngine.POST)
+ || requestMethod.equals(HttpEngine.PUT)
+ || requestMethod.equals(HttpEngine.DELETE)) {
+ cache.remove(key);
}
/*
@@ -90,7 +125,7 @@ public final class HttpResponseCache extends ResponseCache {
* HEAD requests and some POST requests, but the complexity of doing so
* is high and the benefit is low.
*/
- if (!requestMethod.equals("GET")) {
+ if (!requestMethod.equals(HttpEngine.GET)) {
return null;
}
@@ -99,11 +134,22 @@ public final class HttpResponseCache extends ResponseCache {
return null;
}
- return new Entry(uri, httpConnection).asRequest();
+ DiskLruCache.Editor editor = cache.edit(key);
+ new Entry(uri, httpConnection).writeTo(editor);
+ return new CacheRequestImpl(editor);
+ }
+
+ // TODO: add APIs to iterate the cache
+
+ public void close() throws IOException {
+ cache.close();
}
- public synchronized Map<URI, Entry> getContents() {
- return new HashMap<URI, Entry>(entries);
+ /**
+ * Closes this cache and deletes all of its stored contents.
+ */
+ public void delete() throws IOException {
+ cache.delete();
}
/**
@@ -134,167 +180,273 @@ public final class HttpResponseCache extends ResponseCache {
return missCount;
}
- public final class Entry {
- private final ByteArrayOutputStream body = new ByteArrayOutputStream() {
- private boolean closed;
- @Override public void close() throws IOException {
- synchronized (HttpResponseCache.this) {
- if (closed) {
+ private final class CacheRequestImpl extends CacheRequest {
+ private final DiskLruCache.Editor editor;
+ private OutputStream cacheOut;
+ private boolean done;
+ private OutputStream body;
+
+ public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
+ this.editor = editor;
+ this.cacheOut = editor.newOutputStream(1);
+ this.body = new FilterOutputStream(cacheOut) {
+ @Override public void close() throws IOException {
+ if (done) {
return;
}
-
- super.close();
- entries.put(uri, Entry.this);
+ done = true;
successCount++;
- closed = true;
+ super.close();
+ editor.commit();
}
+ };
+ }
+
+ @Override public void abort() {
+ if (done) {
+ return;
}
- };
+ done = true;
+ abortCount++;
+ IoUtils.closeQuietly(cacheOut);
+ try {
+ editor.abort(); // TODO: fix abort() to not throw?
+ } catch (IOException ignored) {
+ }
+ }
- private final String requestMethod;
- private final Map<String, List<String>> responseHeaders;
- private final URI uri;
- private final CacheResponse cacheResponse;
-
- private Entry(URI uri, HttpURLConnection connection) {
- this.uri = uri;
- this.requestMethod = connection.getRequestMethod();
- this.responseHeaders = deepCopy(connection.getHeaderFields());
- this.cacheResponse = connection instanceof HttpsURLConnection
- ? new SecureCacheResponseImpl(responseHeaders, body,
- (HttpsURLConnection) connection)
- : new CacheResponseImpl(responseHeaders, body);
+ @Override public OutputStream getBody() throws IOException {
+ return body;
}
+ }
- public CacheRequest asRequest() {
- return new CacheRequest() {
- private boolean aborted;
- @Override public void abort() {
- synchronized (HttpResponseCache.this) {
- if (aborted) {
- return;
- }
-
- abortCount++;
- aborted = true;
- }
+ private static final class Entry {
+ private final String uri;
+ private final String requestMethod;
+ private final RawHeaders responseHeaders;
+ private final String cipherSuite;
+ private final Certificate[] peerCertificates;
+ private final Certificate[] localCertificates;
+
+ /*
+ * Reads an entry from an input stream. A typical entry looks like this:
+ * http://google.com/foo
+ * GET
+ * HTTP/1.1 200 OK
+ * 3
+ * Content-Type: image/png
+ * Content-Length: 100
+ * Cache-Control: max-age=600
+ *
+ * A typical HTTPS file looks like this:
+ * https://google.com/foo
+ * GET
+ * HTTP/1.1 200 OK
+ * 3
+ * Content-Type: image/png
+ * Content-Length: 100
+ * Cache-Control: max-age=600
+ *
+ * AES_256_WITH_MD5
+ * 2
+ * base64-encoded peerCertificate[0]
+ * base64-encoded peerCertificate[1]
+ * -1
+ *
+ * The file is newline separated. The first three lines are the URL, the
+ * request method and the response status line.
+ *
+ * The next line contains the number of HTTP response header lines. It
+ * is followed by that number of header lines.
+ *
+ * HTTPS responses also contain SSL session information. This begins
+ * with a blank line, and then a line containing the cipher suite. Next
+ * is the length of the peer certificate chain. These certificates are
+ * base64-encoded and appear each on their own line. The next line
+ * contains the length of the local certificate chain. These
+ * certificates are also base64-encoded and appear each on their own
+ * line. A length of -1 is used to encode a null array.
+ */
+ public Entry(InputStream in) throws IOException {
+ try {
+ uri = IoUtils.readLine(in);
+ requestMethod = IoUtils.readLine(in);
+ responseHeaders = new RawHeaders();
+ responseHeaders.setStatusLine(IoUtils.readLine(in));
+ int headerCount = readInt(in);
+ for (int i = 0; i < headerCount; i++) {
+ responseHeaders.addLine(IoUtils.readLine(in));
}
- @Override public OutputStream getBody() throws IOException {
- return body;
+
+ if (isHttps()) {
+ String blank = IoUtils.readLine(in);
+ if (!blank.isEmpty()) {
+ throw new IOException("expected \"\" but was \"" + blank + "\"");
+ }
+ cipherSuite = IoUtils.readLine(in);
+ peerCertificates = readCertArray(in);
+ localCertificates = readCertArray(in);
+ } else {
+ cipherSuite = null;
+ peerCertificates = null;
+ localCertificates = null;
}
- };
+ } finally {
+ in.close();
+ }
}
- public CacheResponse asResponse() {
- return cacheResponse;
+ public Entry(URI uri, HttpURLConnection httpConnection) {
+ this.uri = uri.toString();
+ this.requestMethod = httpConnection.getRequestMethod();
+ this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields());
+
+ if (isHttps()) {
+ HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+ cipherSuite = httpsConnection.getCipherSuite();
+ Certificate[] peerCertificatesNonFinal = null;
+ try {
+ peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+ } catch (SSLPeerUnverifiedException ignored) {
+ }
+ peerCertificates = peerCertificatesNonFinal;
+ localCertificates = httpsConnection.getLocalCertificates();
+ } else {
+ cipherSuite = null;
+ peerCertificates = null;
+ localCertificates = null;
+ }
}
- public byte[] getBytes() {
- return body.toByteArray();
+ public void writeTo(DiskLruCache.Editor editor) throws IOException {
+ OutputStream out = editor.newOutputStream(0);
+ Writer writer = new BufferedWriter(new OutputStreamWriter(out, Charsets.UTF_8));
+ writer.write(uri + '\n');
+ writer.write(requestMethod + '\n');
+ writer.write(responseHeaders.getStatusLine() + '\n');
+ writer.write(Integer.toString(responseHeaders.length()) + '\n');
+ for (int i = 0; i < responseHeaders.length(); i++) {
+ writer.write(responseHeaders.getFieldName(i) + ": "
+ + responseHeaders.getValue(i) + '\n');
+ }
+ if (isHttps()) {
+ writer.write('\n');
+ writer.write(cipherSuite + '\n');
+ writeCertArray(writer, peerCertificates);
+ writeCertArray(writer, localCertificates);
+ }
+ writer.close();
}
- }
-
- private final class CacheResponseImpl extends CacheResponse {
- private final Map<String, List<String>> headers;
- private final ByteArrayOutputStream bytesOut;
- public CacheResponseImpl(Map<String, List<String>> headers,
- ByteArrayOutputStream bytesOut) {
- this.headers = headers;
- this.bytesOut = bytesOut;
+ private boolean isHttps() {
+ return uri.startsWith("https://");
}
- @Override public Map<String, List<String>> getHeaders() {
- return deepCopy(headers);
+ private int readInt(InputStream in) throws IOException {
+ String intString = IoUtils.readLine(in);
+ try {
+ return Integer.parseInt(intString);
+ } catch (NumberFormatException e) {
+ throw new IOException("expected an int but was \"" + intString + "\"");
+ }
}
- @Override public InputStream getBody() {
- return new ByteArrayInputStream(bytesOut.toByteArray());
+ private Certificate[] readCertArray(InputStream in) throws IOException {
+ int length = readInt(in);
+ if (length == -1) {
+ return null;
+ }
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ Certificate[] result = new Certificate[length];
+ for (int i = 0; i < result.length; i++) {
+ String line = IoUtils.readLine(in);
+ byte[] bytes = Base64.decode(line.getBytes(Charsets.US_ASCII));
+ result[i] = certificateFactory.generateCertificate(
+ new ByteArrayInputStream(bytes));
+ }
+ return result;
+ } catch (CertificateException e) {
+ throw new IOException(e);
+ }
}
- }
- private final class SecureCacheResponseImpl extends SecureCacheResponse {
- private final Map<String, List<String>> headers;
- private final ByteArrayOutputStream bytesOut;
- private final String cipherSuite;
- private final Certificate[] localCertificates;
- private final List<Certificate> serverCertificates;
- private final Principal peerPrincipal;
- private final Principal localPrincipal;
-
- public SecureCacheResponseImpl(Map<String, List<String>> headers,
- ByteArrayOutputStream bytesOut,
- HttpsURLConnection httpsConnection) {
- this.headers = headers;
- this.bytesOut = bytesOut;
-
- /*
- * Retrieve the fields eagerly to avoid needing a strong
- * reference to the connection. We do acrobatics for the two
- * methods that can throw so that the cache response also
- * throws.
- */
- List<Certificate> serverCertificatesNonFinal = null;
- try {
- serverCertificatesNonFinal = Arrays.asList(
- httpsConnection.getServerCertificates());
- } catch (SSLPeerUnverifiedException ignored) {
+ private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
+ if (certificates == null) {
+ writer.write("-1\n");
+ return;
}
- Principal peerPrincipalNonFinal = null;
try {
- peerPrincipalNonFinal = httpsConnection.getPeerPrincipal();
- } catch (SSLPeerUnverifiedException ignored) {
+ writer.write(Integer.toString(certificates.length) + '\n');
+ for (Certificate certificate : certificates) {
+ byte[] bytes = certificate.getEncoded();
+ String line = Base64.encode(bytes);
+ writer.write(line + '\n');
+ }
+ } catch (CertificateEncodingException e) {
+ throw new IOException(e);
}
- this.cipherSuite = httpsConnection.getCipherSuite();
- this.localCertificates = httpsConnection.getLocalCertificates();
- this.serverCertificates = serverCertificatesNonFinal;
- this.peerPrincipal = peerPrincipalNonFinal;
- this.localPrincipal = httpsConnection.getLocalPrincipal();
}
- @Override public Map<String, List<String>> getHeaders() {
- return deepCopy(headers);
+ public boolean matches(URI uri, String requestMethod) {
+ return this.uri.equals(uri.toString()) && this.requestMethod.equals(requestMethod);
}
- @Override public InputStream getBody() {
- return new ByteArrayInputStream(bytesOut.toByteArray());
- }
+ public CacheResponse newCacheResponse(final InputStream in) {
+ return new CacheResponse() {
+ @Override public Map<String, List<String>> getHeaders() {
+ return responseHeaders.toMultimap();
+ }
- @Override public String getCipherSuite() {
- return cipherSuite;
+ @Override public InputStream getBody() {
+ return in;
+ }
+ };
}
- @Override public List<Certificate> getLocalCertificateChain() {
- return localCertificates != null
- ? Arrays.asList(localCertificates.clone())
- : null;
- }
+ public SecureCacheResponse newSecureCacheResponse(final InputStream in) {
+ return new SecureCacheResponse() {
+ @Override public Map<String, List<String>> getHeaders() {
+ return responseHeaders.toMultimap();
+ }
- @Override public List<Certificate> getServerCertificateChain()
- throws SSLPeerUnverifiedException {
- if (serverCertificates == null) {
- throw new SSLPeerUnverifiedException(null);
- }
- return new ArrayList<Certificate>(serverCertificates);
- }
+ @Override public InputStream getBody() {
+ return in;
+ }
- @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
- if (peerPrincipal == null) {
- throw new SSLPeerUnverifiedException(null);
- }
- return peerPrincipal;
- }
+ @Override public String getCipherSuite() {
+ return cipherSuite;
+ }
- @Override public Principal getLocalPrincipal() {
- return localPrincipal;
- }
- }
+ @Override public List<Certificate> getServerCertificateChain()
+ throws SSLPeerUnverifiedException {
+ if (peerCertificates == null || peerCertificates.length == 0) {
+ throw new SSLPeerUnverifiedException(null);
+ }
+ return Arrays.asList(peerCertificates.clone());
+ }
- private static Map<String, List<String>> deepCopy(Map<String, List<String>> input) {
- Map<String, List<String>> result = new LinkedHashMap<String, List<String>>(input);
- for (Map.Entry<String, List<String>> entry : result.entrySet()) {
- entry.setValue(new ArrayList<String>(entry.getValue()));
+ @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+ if (peerCertificates == null || peerCertificates.length == 0) {
+ throw new SSLPeerUnverifiedException(null);
+ }
+ return ((X509Certificate) peerCertificates[0]).getSubjectX500Principal();
+ }
+
+ @Override public List<Certificate> getLocalCertificateChain() {
+ if (localCertificates == null || localCertificates.length == 0) {
+ return null;
+ }
+ return Arrays.asList(localCertificates.clone());
+ }
+
+ @Override public Principal getLocalPrincipal() {
+ if (localCertificates == null || localCertificates.length == 0) {
+ return null;
+ }
+ return ((X509Certificate) localCertificates[0]).getSubjectX500Principal();
+ }
+ };
}
- return result;
}
}