/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.okhttp; import com.squareup.okhttp.internal.Base64; import com.squareup.okhttp.internal.DiskLruCache; import com.squareup.okhttp.internal.StrictLineReader; import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.http.HttpEngine; import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; import com.squareup.okhttp.internal.http.HttpsEngine; import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; import com.squareup.okhttp.internal.http.RawHeaders; import com.squareup.okhttp.internal.http.ResponseHeaders; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.CacheRequest; import java.net.CacheResponse; import java.net.HttpURLConnection; import java.net.ResponseCache; import java.net.SecureCacheResponse; import java.net.URI; import java.net.URLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.cert.Certificate; 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.List; import java.util.Map; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import static com.squareup.okhttp.internal.Util.US_ASCII; import static com.squareup.okhttp.internal.Util.UTF_8; /** * Caches HTTP and HTTPS responses to the filesystem so they may be reused, * saving time and bandwidth. * *
The best way to improve the cache hit rate is by configuring the web * server to return cacheable responses. Although this client honors all HTTP/1.1 (RFC 2068) cache * headers, it doesn't cache partial responses. * *
{@code * connection.addRequestProperty("Cache-Control", "no-cache"); * }* If it is only necessary to force a cached response to be validated by the * server, use the more efficient {@code max-age=0} instead:
{@code * connection.addRequestProperty("Cache-Control", "max-age=0"); * }* *
{@code * try { * connection.addRequestProperty("Cache-Control", "only-if-cached"); * InputStream cached = connection.getInputStream(); * // the resource was cached! show it * } catch (FileNotFoundException e) { * // the resource was not cached * } * }* This technique works even better in situations where a stale response is * better than no response. To permit stale cached responses, use the {@code * max-stale} directive with the maximum staleness in seconds:
{@code * int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale * connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale); * }*/ public final class HttpResponseCache extends ResponseCache { private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; // TODO: add APIs to iterate the cache? private static final int VERSION = 201105; private static final int ENTRY_METADATA = 0; private static final int ENTRY_BODY = 1; private static final int ENTRY_COUNT = 2; private final DiskLruCache cache; /* read and write statistics, all guarded by 'this' */ private int writeSuccessCount; private int writeAbortCount; private int networkCount; private int hitCount; private int requestCount; /** * Although this class only exposes the limited ResponseCache API, it * implements the full OkResponseCache interface. This field is used as a * package private handle to the complete implementation. It delegates to * public and private members of this type. */ final OkResponseCache okResponseCache = new OkResponseCache() { @Override public CacheResponse get(URI uri, String requestMethod, Map
{@code * http://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * 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: *
{@code * https://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * 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 two lines are the URL and * the request method. Next is the number of HTTP Vary request header * lines, followed by those lines. * *
Next is the response status line, followed by the number of HTTP * response header lines, followed by those 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 {
StrictLineReader reader = new StrictLineReader(in, US_ASCII);
uri = reader.readLine();
requestMethod = reader.readLine();
varyHeaders = new RawHeaders();
int varyRequestHeaderLineCount = reader.readInt();
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
varyHeaders.addLine(reader.readLine());
}
responseHeaders = new RawHeaders();
responseHeaders.setStatusLine(reader.readLine());
int responseHeaderLineCount = reader.readInt();
for (int i = 0; i < responseHeaderLineCount; i++) {
responseHeaders.addLine(reader.readLine());
}
if (isHttps()) {
String blank = reader.readLine();
if (blank.length() > 0) {
throw new IOException("expected \"\" but was \"" + blank + "\"");
}
cipherSuite = reader.readLine();
peerCertificates = readCertArray(reader);
localCertificates = readCertArray(reader);
} else {
cipherSuite = null;
peerCertificates = null;
localCertificates = null;
}
} finally {
in.close();
}
}
public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
throws IOException {
this.uri = uri.toString();
this.varyHeaders = varyHeaders;
this.requestMethod = httpConnection.getRequestMethod();
this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
SSLSocket sslSocket = getSslSocket(httpConnection);
if (sslSocket != null) {
cipherSuite = sslSocket.getSession().getCipherSuite();
Certificate[] peerCertificatesNonFinal = null;
try {
peerCertificatesNonFinal = sslSocket.getSession().getPeerCertificates();
} catch (SSLPeerUnverifiedException ignored) {
}
peerCertificates = peerCertificatesNonFinal;
localCertificates = sslSocket.getSession().getLocalCertificates();
} else {
cipherSuite = null;
peerCertificates = null;
localCertificates = null;
}
}
/**
* Returns the SSL socket used by {@code httpConnection} for HTTPS, nor null
* if the connection isn't using HTTPS. Since we permit redirects across
* protocols (HTTP to HTTPS or vice versa), the implementation type of the
* connection doesn't necessarily match the implementation type of its HTTP
* engine.
*/
private SSLSocket getSslSocket(HttpURLConnection httpConnection) {
HttpEngine engine = httpConnection instanceof HttpsURLConnectionImpl
? ((HttpsURLConnectionImpl) httpConnection).getHttpEngine()
: ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
return engine instanceof HttpsEngine
? ((HttpsEngine) engine).getSslSocket()
: null;
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
OutputStream out = editor.newOutputStream(ENTRY_METADATA);
Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
writer.write(uri + '\n');
writer.write(requestMethod + '\n');
writer.write(Integer.toString(varyHeaders.length()) + '\n');
for (int i = 0; i < varyHeaders.length(); i++) {
writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\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 boolean isHttps() {
return uri.startsWith("https://");
}
private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
int length = reader.readInt();
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 = reader.readLine();
byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
}
return result;
} catch (CertificateException e) {
throw new IOException(e.getMessage());
}
}
private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
if (certificates == null) {
writer.write("-1\n");
return;
}
try {
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.getMessage());
}
}
public boolean matches(URI uri, String requestMethod,
Map