diff options
Diffstat (limited to 'support/src')
-rw-r--r-- | support/src/test/java/libcore/net/http/HttpResponseCache.java | 440 |
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; } } |