diff --git a/framework/src/com/squareup/okhttp/Connection.java b/framework/src/com/squareup/okhttp/Connection.java index 90424ffe..6a6c84dc 100644 --- a/framework/src/com/squareup/okhttp/Connection.java +++ b/framework/src/com/squareup/okhttp/Connection.java @@ -24,11 +24,11 @@ import com.squareup.okhttp.internal.http.RawHeaders; import com.squareup.okhttp.internal.http.SpdyTransport; import com.squareup.okhttp.internal.spdy.SpdyConnection; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; import java.net.URL; @@ -76,10 +76,7 @@ public final class Connection implements Closeable { 'h', 't', 't', 'p', '/', '1', '.', '1' }; - private final Address address; - private final Proxy proxy; - private final InetSocketAddress inetSocketAddress; - private final boolean modernTls; + private final Route route; private Socket socket; private InputStream in; @@ -89,15 +86,8 @@ public final class Connection implements Closeable { private int httpMinorVersion = 1; // Assume HTTP/1.1 private long idleStartTimeNs; - public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, - boolean modernTls) { - if (address == null) throw new NullPointerException("address == null"); - if (proxy == null) throw new NullPointerException("proxy == null"); - if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null"); - this.address = address; - this.proxy = proxy; - this.inetSocketAddress = inetSocketAddress; - this.modernTls = modernTls; + public Connection(Route route) { + this.route = route; } public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) @@ -106,21 +96,20 @@ public final class Connection implements Closeable { throw new IllegalStateException("already connected"); } connected = true; - socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket(); - socket.connect(inetSocketAddress, connectTimeout); + socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket(); + socket.connect(route.inetSocketAddress, connectTimeout); socket.setSoTimeout(readTimeout); in = socket.getInputStream(); out = socket.getOutputStream(); - if (address.sslSocketFactory != null) { + if (route.address.sslSocketFactory != null) { upgradeToTls(tunnelRequest); } - // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes. - if (!isSpdy()) { - int bufferSize = 128; - in = new BufferedInputStream(in, bufferSize); - } + // Use MTU-sized buffers to send fewer packets. + int mtu = Platform.get().getMtu(socket); + in = new BufferedInputStream(in, mtu); + out = new BufferedOutputStream(out, mtu); } /** @@ -136,16 +125,16 @@ public final class Connection implements Closeable { } // Create the wrapper over connected socket. - socket = address.sslSocketFactory - .createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */); + socket = route.address.sslSocketFactory + .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */); SSLSocket sslSocket = (SSLSocket) socket; - if (modernTls) { - platform.enableTlsExtensions(sslSocket, address.uriHost); + if (route.modernTls) { + platform.enableTlsExtensions(sslSocket, route.address.uriHost); } else { platform.supportTlsIntolerantServer(sslSocket); } - if (modernTls) { + if (route.modernTls) { platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); } @@ -153,18 +142,20 @@ public final class Connection implements Closeable { sslSocket.startHandshake(); // Verify that the socket's certificates are acceptable for the target host. - if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) { - throw new IOException("Hostname '" + address.uriHost + "' was not verified"); + if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) { + throw new IOException("Hostname '" + route.address.uriHost + "' was not verified"); } out = sslSocket.getOutputStream(); in = sslSocket.getInputStream(); byte[] selectedProtocol; - if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { + if (route.modernTls + && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { if (Arrays.equals(selectedProtocol, SPDY3)) { sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. - spdyConnection = new SpdyConnection.Builder(address.getUriHost(), true, in, out).build(); + spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out) + .build(); } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { throw new IOException( "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1")); @@ -181,29 +172,9 @@ public final class Connection implements Closeable { socket.close(); } - /** - * Returns the proxy that this connection is using. - * - * Warning: This may be different than the proxy returned - * by {@link #getAddress}! That is the proxy that the user asked to be - * connected to; this returns the proxy that they were actually connected - * to. The two may disagree when a proxy selector selects a different proxy - * for a connection. - */ - public Proxy getProxy() { - return proxy; - } - - public Address getAddress() { - return address; - } - - public InetSocketAddress getSocketAddress() { - return inetSocketAddress; - } - - public boolean isModernTls() { - return modernTls; + /** Returns the route used by this connection. */ + public Route getRoute() { + return route; } /** @@ -284,7 +255,7 @@ public final class Connection implements Closeable { * we must avoid buffering bytes intended for the higher-level protocol. */ public boolean requiresTunnel() { - return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP; + return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP; } /** @@ -304,9 +275,8 @@ public final class Connection implements Closeable { case HTTP_PROXY_AUTH: requestHeaders = new RawHeaders(requestHeaders); URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); - boolean credentialsFound = - HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, responseHeaders, requestHeaders, - proxy, url); + boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, + responseHeaders, requestHeaders, route.proxy, url); if (credentialsFound) { continue; } else { diff --git a/framework/src/com/squareup/okhttp/ConnectionPool.java b/framework/src/com/squareup/okhttp/ConnectionPool.java index 4eff4ec7..933bd737 100644 --- a/framework/src/com/squareup/okhttp/ConnectionPool.java +++ b/framework/src/com/squareup/okhttp/ConnectionPool.java @@ -1,3 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.Platform; @@ -164,7 +180,7 @@ public class ConnectionPool { for (ListIterator i = connections.listIterator(connections.size()); i.hasPrevious(); ) { Connection connection = i.previous(); - if (!connection.getAddress().equals(address) + if (!connection.getRoute().getAddress().equals(address) || !connection.isAlive() || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) { continue; diff --git a/framework/src/com/squareup/okhttp/HttpResponseCache.java b/framework/src/com/squareup/okhttp/HttpResponseCache.java new file mode 100644 index 00000000..a6d380ab --- /dev/null +++ b/framework/src/com/squareup/okhttp/HttpResponseCache.java @@ -0,0 +1,693 @@ +/* + * 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.HttpsURLConnectionImpl; +import com.squareup.okhttp.internal.http.OkResponseCache; +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.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; + +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. + * + *

Cache Optimization

+ * To measure cache effectiveness, this class tracks three statistics: + * + * Sometimes a request will result in a conditional cache hit. If the cache + * contains a stale copy of the response, the client will issue a conditional + * {@code GET}. The server will then send either the updated response if it has + * changed, or a short 'not modified' response if the client's copy is still + * valid. Such responses increment both the network count and hit count. + * + *

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. + * + *

Force a Network Response

+ * In some situations, such as after a user clicks a 'refresh' button, it may be + * necessary to skip the cache, and fetch data directly from the server. To force + * a full refresh, add the {@code no-cache} directive:
   {@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");
+ * }
+ * + *

Force a Cache Response

+ * Sometimes you'll want to show resources if they are available immediately, + * but not otherwise. This can be used so your application can show + * something while waiting for the latest data to be downloaded. To + * restrict a request to locally-cached resources, add the {@code + * only-if-cached} directive:
   {@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> requestHeaders) throws IOException { + return HttpResponseCache.this.get(uri, requestMethod, requestHeaders); + } + + @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { + return HttpResponseCache.this.put(uri, connection); + } + + @Override public void update( + CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException { + HttpResponseCache.this.update(conditionalCacheHit, connection); + } + + @Override public void trackConditionalCacheHit() { + HttpResponseCache.this.trackConditionalCacheHit(); + } + + @Override public void trackResponse(ResponseSource source) { + HttpResponseCache.this.trackResponse(source); + } + }; + + public HttpResponseCache(File directory, long maxSize) throws IOException { + cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); + } + + private String uriToKey(URI uri) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8")); + return bytesToHexString(md5bytes); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + private static String bytesToHexString(byte[] bytes) { + char[] digits = DIGITS; + char[] buf = new char[bytes.length * 2]; + int c = 0; + for (byte b : bytes) { + buf[c++] = digits[(b >> 4) & 0xf]; + buf[c++] = digits[b & 0xf]; + } + return new String(buf); + } + + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) { + String key = uriToKey(uri); + DiskLruCache.Snapshot snapshot; + Entry entry; + try { + snapshot = cache.get(key); + if (snapshot == null) { + return null; + } + entry = new Entry(snapshot.getInputStream(ENTRY_METADATA)); + } catch (IOException e) { + // Give up because the cache cannot be read. + return null; + } + + if (!entry.matches(uri, requestMethod, requestHeaders)) { + snapshot.close(); + return null; + } + + return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot) + : new EntryCacheResponse(entry, snapshot); + } + + @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { + if (!(urlConnection instanceof HttpURLConnection)) { + return null; + } + + HttpURLConnection httpConnection = (HttpURLConnection) urlConnection; + String requestMethod = httpConnection.getRequestMethod(); + String key = uriToKey(uri); + + if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals( + "DELETE")) { + try { + cache.remove(key); + } catch (IOException ignored) { + // The cache cannot be written. + } + return null; + } else if (!requestMethod.equals("GET")) { + // Don't cache non-GET responses. We're technically allowed to cache + // HEAD requests and some POST requests, but the complexity of doing + // so is high and the benefit is low. + return null; + } + + HttpEngine httpEngine = getHttpEngine(httpConnection); + if (httpEngine == null) { + // Don't cache unless the HTTP implementation is ours. + return null; + } + + ResponseHeaders response = httpEngine.getResponseHeaders(); + if (response.hasVaryAll()) { + return null; + } + + RawHeaders varyHeaders = + httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields()); + Entry entry = new Entry(uri, varyHeaders, httpConnection); + DiskLruCache.Editor editor = null; + try { + editor = cache.edit(key); + if (editor == null) { + return null; + } + entry.writeTo(editor); + return new CacheRequestImpl(editor); + } catch (IOException e) { + abortQuietly(editor); + return null; + } + } + + private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection) + throws IOException { + HttpEngine httpEngine = getHttpEngine(httpConnection); + URI uri = httpEngine.getUri(); + ResponseHeaders response = httpEngine.getResponseHeaders(); + RawHeaders varyHeaders = + httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields()); + Entry entry = new Entry(uri, varyHeaders, httpConnection); + DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse) + ? ((EntryCacheResponse) conditionalCacheHit).snapshot + : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot; + DiskLruCache.Editor editor = null; + try { + editor = snapshot.edit(); // returns null if snapshot is not current + if (editor != null) { + entry.writeTo(editor); + editor.commit(); + } + } catch (IOException e) { + abortQuietly(editor); + } + } + + private void abortQuietly(DiskLruCache.Editor editor) { + // Give up because the cache cannot be written. + try { + if (editor != null) { + editor.abort(); + } + } catch (IOException ignored) { + } + } + + private HttpEngine getHttpEngine(URLConnection httpConnection) { + if (httpConnection instanceof HttpURLConnectionImpl) { + return ((HttpURLConnectionImpl) httpConnection).getHttpEngine(); + } else if (httpConnection instanceof HttpsURLConnectionImpl) { + return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine(); + } else { + return null; + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + cache.delete(); + } + + public synchronized int getWriteAbortCount() { + return writeAbortCount; + } + + public synchronized int getWriteSuccessCount() { + return writeSuccessCount; + } + + private synchronized void trackResponse(ResponseSource source) { + requestCount++; + + switch (source) { + case CACHE: + hitCount++; + break; + case CONDITIONAL_CACHE: + case NETWORK: + networkCount++; + break; + } + } + + private synchronized void trackConditionalCacheHit() { + hitCount++; + } + + public synchronized int getNetworkCount() { + return networkCount; + } + + public synchronized int getHitCount() { + return hitCount; + } + + public synchronized int getRequestCount() { + return requestCount; + } + + 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(ENTRY_BODY); + this.body = new FilterOutputStream(cacheOut) { + @Override public void close() throws IOException { + synchronized (HttpResponseCache.this) { + if (done) { + return; + } + done = true; + writeSuccessCount++; + } + super.close(); + editor.commit(); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + // Since we don't override "write(int oneByte)", we can write directly to "out" + // and avoid the inefficient implementation from the FilterOutputStream. + out.write(buffer, offset, length); + } + }; + } + + @Override public void abort() { + synchronized (HttpResponseCache.this) { + if (done) { + return; + } + done = true; + writeAbortCount++; + } + Util.closeQuietly(cacheOut); + try { + editor.abort(); + } catch (IOException ignored) { + } + } + + @Override public OutputStream getBody() throws IOException { + return body; + } + } + + private static final class Entry { + private final String uri; + private final RawHeaders varyHeaders; + 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: + *
{@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); + + 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 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> requestHeaders) { + return this.uri.equals(uri.toString()) + && this.requestMethod.equals(requestMethod) + && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false), + requestHeaders); + } + } + + /** + * Returns an input stream that reads the body of a snapshot, closing the + * snapshot when the stream is closed. + */ + private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) { + return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) { + @Override public void close() throws IOException { + snapshot.close(); + super.close(); + } + }; + } + + static class EntryCacheResponse extends CacheResponse { + private final Entry entry; + private final DiskLruCache.Snapshot snapshot; + private final InputStream in; + + public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { + this.entry = entry; + this.snapshot = snapshot; + this.in = newBodyInputStream(snapshot); + } + + @Override public Map> getHeaders() { + return entry.responseHeaders.toMultimap(true); + } + + @Override public InputStream getBody() { + return in; + } + } + + static class EntrySecureCacheResponse extends SecureCacheResponse { + private final Entry entry; + private final DiskLruCache.Snapshot snapshot; + private final InputStream in; + + public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { + this.entry = entry; + this.snapshot = snapshot; + this.in = newBodyInputStream(snapshot); + } + + @Override public Map> getHeaders() { + return entry.responseHeaders.toMultimap(true); + } + + @Override public InputStream getBody() { + return in; + } + + @Override public String getCipherSuite() { + return entry.cipherSuite; + } + + @Override public List getServerCertificateChain() + throws SSLPeerUnverifiedException { + if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { + throw new SSLPeerUnverifiedException(null); + } + return Arrays.asList(entry.peerCertificates.clone()); + } + + @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { + throw new SSLPeerUnverifiedException(null); + } + return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal(); + } + + @Override public List getLocalCertificateChain() { + if (entry.localCertificates == null || entry.localCertificates.length == 0) { + return null; + } + return Arrays.asList(entry.localCertificates.clone()); + } + + @Override public Principal getLocalPrincipal() { + if (entry.localCertificates == null || entry.localCertificates.length == 0) { + return null; + } + return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal(); + } + } +} diff --git a/framework/src/com/squareup/okhttp/OkHttpClient.java b/framework/src/com/squareup/okhttp/OkHttpClient.java index d21cdb71..7834bd6b 100644 --- a/framework/src/com/squareup/okhttp/OkHttpClient.java +++ b/framework/src/com/squareup/okhttp/OkHttpClient.java @@ -17,12 +17,17 @@ package com.squareup.okhttp; import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; +import com.squareup.okhttp.internal.http.OkResponseCache; +import com.squareup.okhttp.internal.http.OkResponseCacheAdapter; import java.net.CookieHandler; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.ProxySelector; import java.net.ResponseCache; import java.net.URL; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; @@ -30,6 +35,7 @@ import javax.net.ssl.SSLSocketFactory; /** Configures and creates HTTP connections. */ public final class OkHttpClient { private Proxy proxy; + private Set failedRoutes = Collections.synchronizedSet(new LinkedHashSet()); private ProxySelector proxySelector; private CookieHandler cookieHandler; private ResponseCache responseCache; @@ -102,6 +108,16 @@ public final class OkHttpClient { return responseCache; } + private OkResponseCache okResponseCache() { + if (responseCache instanceof HttpResponseCache) { + return ((HttpResponseCache) responseCache).okResponseCache; + } else if (responseCache != null) { + return new OkResponseCacheAdapter(responseCache); + } else { + return null; + } + } + /** * Sets the socket factory used to secure HTTPS connections. * @@ -166,22 +182,24 @@ public final class OkHttpClient { public HttpURLConnection open(URL url) { String protocol = url.getProtocol(); + OkHttpClient copy = copyWithDefaults(); if (protocol.equals("http")) { - return new HttpURLConnectionImpl(url, copyWithDefaults()); + return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes); } else if (protocol.equals("https")) { - return new HttpsURLConnectionImpl(url, copyWithDefaults()); + return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes); } else { throw new IllegalArgumentException("Unexpected protocol: " + protocol); } } /** - * Returns a copy of this OkHttpClient that uses the system-wide default for + * Returns a shallow copy of this OkHttpClient that uses the system-wide default for * each field that hasn't been explicitly configured. */ private OkHttpClient copyWithDefaults() { OkHttpClient result = new OkHttpClient(); result.proxy = proxy; + result.failedRoutes = failedRoutes; result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault(); result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault(); result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault(); diff --git a/framework/src/com/squareup/okhttp/Route.java b/framework/src/com/squareup/okhttp/Route.java new file mode 100644 index 00000000..6968c604 --- /dev/null +++ b/framework/src/com/squareup/okhttp/Route.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * 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 java.net.InetSocketAddress; +import java.net.Proxy; + +/** Represents the route used by a connection to reach an endpoint. */ +public class Route { + final Address address; + final Proxy proxy; + final InetSocketAddress inetSocketAddress; + final boolean modernTls; + + public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, + boolean modernTls) { + if (address == null) throw new NullPointerException("address == null"); + if (proxy == null) throw new NullPointerException("proxy == null"); + if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null"); + this.address = address; + this.proxy = proxy; + this.inetSocketAddress = inetSocketAddress; + this.modernTls = modernTls; + } + + /** Returns the {@link Address} of this route. */ + public Address getAddress() { + return address; + } + + /** + * Returns the {@link Proxy} of this route. + * + * Warning: This may be different than the proxy returned + * by {@link #getAddress}! That is the proxy that the user asked to be + * connected to; this returns the proxy that they were actually connected + * to. The two may disagree when a proxy selector selects a different proxy + * for a connection. + */ + public Proxy getProxy() { + return proxy; + } + + /** Returns the {@link InetSocketAddress} of this route. */ + public InetSocketAddress getSocketAddress() { + return inetSocketAddress; + } + + /** Returns true if this route uses modern tls. */ + public boolean isModernTls() { + return modernTls; + } + + /** Returns a copy of this route with flipped tls mode. */ + public Route flipTlsMode() { + return new Route(address, proxy, inetSocketAddress, !modernTls); + } + + @Override public boolean equals(Object obj) { + if (obj instanceof Route) { + Route other = (Route) obj; + return (address.equals(other.address) + && proxy.equals(other.proxy) + && inetSocketAddress.equals(other.inetSocketAddress) + && modernTls == other.modernTls); + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + address.hashCode(); + result = 31 * result + proxy.hashCode(); + result = 31 * result + inetSocketAddress.hashCode(); + result = result + (modernTls ? (31 * result) : 0); + return result; + } +} diff --git a/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java new file mode 100644 index 00000000..78c9691e --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java @@ -0,0 +1,45 @@ +/* + * 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.internal; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An output stream for an HTTP request body. + * + *

Since a single socket's output stream may be used to write multiple HTTP + * requests to the same server, subclasses should not close the socket stream. + */ +public abstract class AbstractOutputStream extends OutputStream { + protected boolean closed; + + @Override public final void write(int data) throws IOException { + write(new byte[] { (byte) data }); + } + + protected final void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + } + + /** Returns true if this stream was closed locally. */ + public boolean isClosed() { + return closed; + } +} diff --git a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java index 00fe2f18..f7fcb1ed 100644 --- a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java +++ b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java @@ -23,7 +23,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; @@ -32,23 +31,22 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; - -import static com.squareup.okhttp.internal.Util.UTF_8; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A cache that uses a bounded amount of space on a filesystem. Each cache - * entry has a string key and a fixed number of values. Values are byte - * sequences, accessible as streams or files. Each value must be between {@code - * 0} and {@code Integer.MAX_VALUE} bytes in length. + * entry has a string key and a fixed number of values. Each key must match + * the regex [a-z0-9_-]{1,64}. Values are byte sequences, + * accessible as streams or files. Each value must be between {@code 0} and + * {@code Integer.MAX_VALUE} bytes in length. * *

The cache stores its data in a directory on the filesystem. This * directory must be exclusive to the cache; the cache may delete or overwrite @@ -66,12 +64,12 @@ import static com.squareup.okhttp.internal.Util.UTF_8; * entry may have only one editor at one time; if a value is not available to be * edited then {@link #edit} will return null. *

    - *
  • When an entry is being created it is necessary to - * supply a full set of values; the empty value should be used as a - * placeholder if necessary. - *
  • When an entry is being edited, it is not necessary - * to supply data for every value; values default to their previous - * value. + *
  • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
  • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. *
* Every {@link #edit} call must be matched by a call to {@link Editor#commit} * or {@link Editor#abort}. Committing is atomic: a read observes the full set @@ -89,58 +87,63 @@ import static com.squareup.okhttp.internal.Util.UTF_8; */ public final class DiskLruCache implements Closeable { static final String JOURNAL_FILE = "journal"; - static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String JOURNAL_FILE_TEMP = "journal.tmp"; + static final String JOURNAL_FILE_BACKUP = "journal.bkp"; static final String MAGIC = "libcore.io.DiskLruCache"; static final String VERSION_1 = "1"; static final long ANY_SEQUENCE_NUMBER = -1; + static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}"); private static final String CLEAN = "CLEAN"; private static final String DIRTY = "DIRTY"; private static final String REMOVE = "REMOVE"; private static final String READ = "READ"; - // This cache uses a journal file named "journal". A typical journal file - // looks like this: - // libcore.io.DiskLruCache - // 1 - // 100 - // 2 - // - // CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 - // DIRTY 335c4c6028171cfddfbaae1a9c313c52 - // CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 - // REMOVE 335c4c6028171cfddfbaae1a9c313c52 - // DIRTY 1ab96a171faeeee38496d8b330771a7a - // CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 - // READ 335c4c6028171cfddfbaae1a9c313c52 - // READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 - // - // The first five lines of the journal form its header. They are the - // constant string "libcore.io.DiskLruCache", the disk cache's version, - // the application's version, the value count, and a blank line. - // - // Each of the subsequent lines in the file is a record of the state of a - // cache entry. Each line contains space-separated values: a state, a key, - // and optional state-specific values. - // o DIRTY lines track that an entry is actively being created or updated. - // Every successful DIRTY action should be followed by a CLEAN or REMOVE - // action. DIRTY lines without a matching CLEAN or REMOVE indicate that - // temporary files may need to be deleted. - // o CLEAN lines track a cache entry that has been successfully published - // and may be read. A publish line is followed by the lengths of each of - // its values. - // o READ lines track accesses for LRU. - // o REMOVE lines track entries that have been deleted. - // - // The journal file is appended to as cache operations occur. The journal may - // occasionally be compacted by dropping redundant lines. A temporary file named - // "journal.tmp" will be used during compaction; that file should be deleted if - // it exists when the cache is opened. + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ private final File directory; private final File journalFile; private final File journalFileTmp; + private final File journalFileBackup; private final int appVersion; - private final long maxSize; + private long maxSize; private final int valueCount; private long size = 0; private Writer journalWriter; @@ -156,13 +159,13 @@ public final class DiskLruCache implements Closeable { private long nextSequenceNumber = 0; /** This cache uses a single background thread to evict entries. */ - private final ExecutorService executorService = + final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); private final Callable cleanupCallable = new Callable() { - @Override public Void call() throws Exception { + public Void call() throws Exception { synchronized (DiskLruCache.this) { if (journalWriter == null) { - return null; // closed + return null; // Closed. } trimToSize(); if (journalRebuildRequired()) { @@ -178,7 +181,8 @@ public final class DiskLruCache implements Closeable { this.directory = directory; this.appVersion = appVersion; this.journalFile = new File(directory, JOURNAL_FILE); - this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); + this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); this.valueCount = valueCount; this.maxSize = maxSize; } @@ -201,26 +205,35 @@ public final class DiskLruCache implements Closeable { throw new IllegalArgumentException("valueCount <= 0"); } - // prefer to pick up where we left off + // If a bkp file exists, use it instead. + File backupFile = new File(directory, JOURNAL_FILE_BACKUP); + if (backupFile.exists()) { + File journalFile = new File(directory, JOURNAL_FILE); + // If journal file also exists just delete backup file. + if (journalFile.exists()) { + backupFile.delete(); + } else { + renameTo(backupFile, journalFile, false); + } + } + + // Prefer to pick up where we left off. DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); if (cache.journalFile.exists()) { try { cache.readJournal(); cache.processJournal(); - cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true)); + cache.journalWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII)); return cache; } catch (IOException journalIsCorrupt) { - Platform.get() - .logW("DiskLruCache " - + directory - + " is corrupt: " - + journalIsCorrupt.getMessage() - + ", removing"); + Platform.get().logW("DiskLruCache " + directory + " is corrupt: " + + journalIsCorrupt.getMessage() + ", removing"); cache.delete(); } } - // create a new empty cache + // Create a new empty cache. directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); @@ -235,42 +248,47 @@ public final class DiskLruCache implements Closeable { String appVersionString = reader.readLine(); String valueCountString = reader.readLine(); String blank = reader.readLine(); - if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion) - .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !"" - .equals(blank)) { - throw new IOException("unexpected journal header: [" - + magic - + ", " - + version - + ", " - + valueCountString - + ", " - + blank - + "]"); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + + valueCountString + ", " + blank + "]"); } + int lineCount = 0; while (true) { try { readJournalLine(reader.readLine()); + lineCount++; } catch (EOFException endOfJournal) { break; } } + redundantOpCount = lineCount - lruEntries.size(); } finally { Util.closeQuietly(reader); } } private void readJournalLine(String line) throws IOException { - String[] parts = line.split(" "); - if (parts.length < 2) { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { throw new IOException("unexpected journal line: " + line); } - String key = parts[1]; - if (parts[0].equals(REMOVE) && parts.length == 2) { - lruEntries.remove(key); - return; + int keyBegin = firstSpace + 1; + int secondSpace = line.indexOf(' ', keyBegin); + final String key; + if (secondSpace == -1) { + key = line.substring(keyBegin); + if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { + lruEntries.remove(key); + return; + } + } else { + key = line.substring(keyBegin, secondSpace); } Entry entry = lruEntries.get(key); @@ -279,14 +297,15 @@ public final class DiskLruCache implements Closeable { lruEntries.put(key, entry); } - if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { + String[] parts = line.substring(secondSpace + 1).split(" "); entry.readable = true; entry.currentEditor = null; - entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length)); - } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.setLengths(parts); + } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { entry.currentEditor = new Editor(entry); - } else if (parts[0].equals(READ) && parts.length == 2) { - // this work was already done by calling lruEntries.get() + } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { + // This work was already done by calling lruEntries.get(). } else { throw new IOException("unexpected journal line: " + line); } @@ -324,32 +343,53 @@ public final class DiskLruCache implements Closeable { journalWriter.close(); } - Writer writer = new BufferedWriter(new FileWriter(journalFileTmp)); - writer.write(MAGIC); - writer.write("\n"); - writer.write(VERSION_1); - writer.write("\n"); - writer.write(Integer.toString(appVersion)); - writer.write("\n"); - writer.write(Integer.toString(valueCount)); - writer.write("\n"); - writer.write("\n"); + Writer writer = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); + try { + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); - for (Entry entry : lruEntries.values()) { - if (entry.currentEditor != null) { - writer.write(DIRTY + ' ' + entry.key + '\n'); - } else { - writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } } + } finally { + writer.close(); } - writer.close(); - journalFileTmp.renameTo(journalFile); - journalWriter = new BufferedWriter(new FileWriter(journalFile, true)); + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); + } + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); } private static void deleteIfExists(File file) throws IOException { - file.delete(); + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { + if (deleteDestination) { + deleteIfExists(to); + } + if (!from.renameTo(to)) { + throw new IOException(); + } } /** @@ -378,7 +418,14 @@ public final class DiskLruCache implements Closeable { ins[i] = new FileInputStream(entry.getCleanFile(i)); } } catch (FileNotFoundException e) { - // a file must have been deleted manually! + // A file must have been deleted manually! + for (int i = 0; i < valueCount; i++) { + if (ins[i] != null) { + Util.closeQuietly(ins[i]); + } else { + break; + } + } return null; } @@ -388,7 +435,7 @@ public final class DiskLruCache implements Closeable { executorService.submit(cleanupCallable); } - return new Snapshot(key, entry.sequenceNumber, ins); + return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); } /** @@ -405,19 +452,19 @@ public final class DiskLruCache implements Closeable { Entry entry = lruEntries.get(key); if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { - return null; // snapshot is stale + return null; // Snapshot is stale. } if (entry == null) { entry = new Entry(key); lruEntries.put(key, entry); } else if (entry.currentEditor != null) { - return null; // another edit is in progress + return null; // Another edit is in progress. } Editor editor = new Editor(entry); entry.currentEditor = editor; - // flush the journal before creating files to prevent file leaks + // Flush the journal before creating files to prevent file leaks. journalWriter.write(DIRTY + ' ' + key + '\n'); journalWriter.flush(); return editor; @@ -432,10 +479,19 @@ public final class DiskLruCache implements Closeable { * Returns the maximum number of bytes that this cache should use to store * its data. */ - public long maxSize() { + public long getMaxSize() { return maxSize; } + /** + * Changes the maximum number of bytes the cache can store and queues a job + * to trim the existing store, if necessary. + */ + public synchronized void setMaxSize(long maxSize) { + this.maxSize = maxSize; + executorService.submit(cleanupCallable); + } + /** * Returns the number of bytes currently being used to store the values in * this cache. This may be greater than the max size if a background @@ -451,7 +507,7 @@ public final class DiskLruCache implements Closeable { throw new IllegalStateException(); } - // if this edit is creating the entry for the first time, every index must have a value + // If this edit is creating the entry for the first time, every index must have a value. if (success && !entry.readable) { for (int i = 0; i < valueCount; i++) { if (!editor.written[i]) { @@ -460,7 +516,6 @@ public final class DiskLruCache implements Closeable { } if (!entry.getDirtyFile(i).exists()) { editor.abort(); - Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i); return; } } @@ -494,6 +549,7 @@ public final class DiskLruCache implements Closeable { lruEntries.remove(entry.key); journalWriter.write(REMOVE + ' ' + entry.key + '\n'); } + journalWriter.flush(); if (size > maxSize || journalRebuildRequired()) { executorService.submit(cleanupCallable); @@ -506,7 +562,8 @@ public final class DiskLruCache implements Closeable { */ private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; - return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size(); + return redundantOpCount >= redundantOpCompactThreshold // + && redundantOpCount >= lruEntries.size(); } /** @@ -564,7 +621,7 @@ public final class DiskLruCache implements Closeable { /** Closes this cache. Stored values will remain on the filesystem. */ public synchronized void close() throws IOException { if (journalWriter == null) { - return; // already closed + return; // Already closed. } for (Entry entry : new ArrayList(lruEntries.values())) { if (entry.currentEditor != null) { @@ -594,14 +651,14 @@ public final class DiskLruCache implements Closeable { } private void validateKey(String key) { - if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { - throw new IllegalArgumentException( - "keys must not contain spaces or newlines: \"" + key + "\""); + Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); + if (!matcher.matches()) { + throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\""); } } private static String inputStreamToString(InputStream in) throws IOException { - return Util.readFully(new InputStreamReader(in, UTF_8)); + return Util.readFully(new InputStreamReader(in, Util.UTF_8)); } /** A snapshot of the values for an entry. */ @@ -609,11 +666,13 @@ public final class DiskLruCache implements Closeable { private final String key; private final long sequenceNumber; private final InputStream[] ins; + private final long[] lengths; - private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { this.key = key; this.sequenceNumber = sequenceNumber; this.ins = ins; + this.lengths = lengths; } /** @@ -635,18 +694,31 @@ public final class DiskLruCache implements Closeable { return inputStreamToString(getInputStream(index)); } - @Override public void close() { + /** Returns the byte length of the value for {@code index}. */ + public long getLength(int index) { + return lengths[index]; + } + + public void close() { for (InputStream in : ins) { Util.closeQuietly(in); } } } + private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { + @Override + public void write(int b) throws IOException { + // Eat all writes silently. Nom nom. + } + }; + /** Edits the values for an entry. */ public final class Editor { private final Entry entry; private final boolean[] written; private boolean hasErrors; + private boolean committed; private Editor(Entry entry) { this.entry = entry; @@ -665,7 +737,11 @@ public final class DiskLruCache implements Closeable { if (!entry.readable) { return null; } - return new FileInputStream(entry.getCleanFile(index)); + try { + return new FileInputStream(entry.getCleanFile(index)); + } catch (FileNotFoundException e) { + return null; + } } } @@ -693,7 +769,21 @@ public final class DiskLruCache implements Closeable { if (!entry.readable) { written[index] = true; } - return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + File dirtyFile = entry.getDirtyFile(index); + FileOutputStream outputStream; + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e) { + // Attempt to recreate the cache directory. + directory.mkdirs(); + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e2) { + // We are unable to recover. Silently eat the writes. + return NULL_OUTPUT_STREAM; + } + } + return new FaultHidingOutputStream(outputStream); } } @@ -701,7 +791,7 @@ public final class DiskLruCache implements Closeable { public void set(int index, String value) throws IOException { Writer writer = null; try { - writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); writer.write(value); } finally { Util.closeQuietly(writer); @@ -715,10 +805,11 @@ public final class DiskLruCache implements Closeable { public void commit() throws IOException { if (hasErrors) { completeEdit(this, false); - remove(entry.key); // the previous entry is stale + remove(entry.key); // The previous entry is stale. } else { completeEdit(this, true); } + committed = true; } /** @@ -729,7 +820,16 @@ public final class DiskLruCache implements Closeable { completeEdit(this, false); } - private final class FaultHidingOutputStream extends FilterOutputStream { + public void abortUnlessCommitted() { + if (!committed) { + try { + abort(); + } catch (IOException ignored) { + } + } + } + + private class FaultHidingOutputStream extends FilterOutputStream { private FaultHidingOutputStream(OutputStream out) { super(out); } @@ -812,7 +912,7 @@ public final class DiskLruCache implements Closeable { } private IOException invalidLengths(String[] strings) throws IOException { - throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); } public File getCleanFile(int i) { diff --git a/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java new file mode 100644 index 00000000..c32b27ae --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * 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.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; + +/** + * An output stream wrapper that recovers from failures in the underlying stream + * by replacing it with another stream. This class buffers a fixed amount of + * data under the assumption that failures occur early in a stream's life. + * If a failure occurs after the buffer has been exhausted, no recovery is + * attempted. + * + *

Subclasses must override {@link #replacementStream} which will request a + * replacement stream each time an {@link IOException} is encountered on the + * current stream. + */ +public abstract class FaultRecoveringOutputStream extends AbstractOutputStream { + private final int maxReplayBufferLength; + + /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */ + private ByteArrayOutputStream replayBuffer; + private OutputStream out; + + /** + * @param maxReplayBufferLength the maximum number of successfully written + * bytes to buffer so they can be replayed in the event of an error. + * Failure recoveries are not possible once this limit has been exceeded. + */ + public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) { + if (maxReplayBufferLength < 0) throw new IllegalArgumentException(); + this.maxReplayBufferLength = maxReplayBufferLength; + this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength); + this.out = out; + } + + @Override public final void write(byte[] buffer, int offset, int count) throws IOException { + if (closed) throw new IOException("stream closed"); + checkOffsetAndCount(buffer.length, offset, count); + + while (true) { + try { + out.write(buffer, offset, count); + + if (replayBuffer != null) { + if (count + replayBuffer.size() > maxReplayBufferLength) { + // Failure recovery is no longer possible once we overflow the replay buffer. + replayBuffer = null; + } else { + // Remember the written bytes to the replay buffer. + replayBuffer.write(buffer, offset, count); + } + } + return; + } catch (IOException e) { + if (!recover(e)) throw e; + } + } + } + + @Override public final void flush() throws IOException { + if (closed) { + return; // don't throw; this stream might have been closed on the caller's behalf + } + while (true) { + try { + out.flush(); + return; + } catch (IOException e) { + if (!recover(e)) throw e; + } + } + } + + @Override public final void close() throws IOException { + if (closed) { + return; + } + while (true) { + try { + out.close(); + closed = true; + return; + } catch (IOException e) { + if (!recover(e)) throw e; + } + } + } + + /** + * Attempt to replace {@code out} with another equivalent stream. Returns true + * if a suitable replacement stream was found. + */ + private boolean recover(IOException e) { + if (replayBuffer == null) { + return false; // Can't recover because we've dropped data that we would need to replay. + } + + while (true) { + OutputStream replacementStream = null; + try { + replacementStream = replacementStream(e); + if (replacementStream == null) { + return false; + } + replaceStream(replacementStream); + return true; + } catch (IOException replacementStreamFailure) { + // The replacement was also broken. Loop to ask for another replacement. + Util.closeQuietly(replacementStream); + e = replacementStreamFailure; + } + } + } + + /** + * Returns true if errors in the underlying stream can currently be recovered. + */ + public boolean isRecoverable() { + return replayBuffer != null; + } + + /** + * Replaces the current output stream with {@code replacementStream}, writing + * any replay bytes to it if they exist. The current output stream is closed. + */ + public final void replaceStream(OutputStream replacementStream) throws IOException { + if (!isRecoverable()) { + throw new IllegalStateException(); + } + if (this.out == replacementStream) { + return; // Don't replace a stream with itself. + } + replayBuffer.writeTo(replacementStream); + Util.closeQuietly(out); + out = replacementStream; + } + + /** + * Returns a replacement output stream to recover from {@code e} thrown by the + * previous stream. Returns a new OutputStream if recovery was successful, in + * which case all previously-written data will be replayed. Returns null if + * the failure cannot be recovered. + */ + protected abstract OutputStream replacementStream(IOException e) throws IOException; +} diff --git a/framework/src/com/squareup/okhttp/internal/Platform.java b/framework/src/com/squareup/okhttp/internal/Platform.java index 75cd66f7..6b4ac343 100644 --- a/framework/src/com/squareup/okhttp/internal/Platform.java +++ b/framework/src/com/squareup/okhttp/internal/Platform.java @@ -17,6 +17,7 @@ package com.squareup.okhttp.internal; import com.squareup.okhttp.OkHttpClient; +import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; @@ -24,6 +25,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.net.NetworkInterface; import java.net.Socket; import java.net.SocketException; import java.net.URI; @@ -123,8 +125,27 @@ public class Platform { } } + /** + * Returns the maximum transmission unit of the network interface used by + * {@code socket}, or a reasonable default if this platform doesn't expose the + * MTU to the application layer. + * + *

The returned value should only be used as an optimization; such as to + * size buffers efficiently. + */ + public int getMtu(Socket socket) throws IOException { + return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE. + } + /** Attempt to match the host runtime to a capable Platform implementation. */ private static Platform findPlatform() { + Method getMtu; + try { + getMtu = NetworkInterface.class.getMethod("getMTU"); + } catch (NoSuchMethodException e) { + return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier. + } + // Attempt to find Android 2.3+ APIs. Class openSslSocketClass; Method setUseSessionTickets; @@ -138,10 +159,10 @@ public class Platform { try { Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); - return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols, - getNpnSelectedProtocol); + return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname, + setNpnProtocols, getNpnSelectedProtocol); } catch (NoSuchMethodException ignored) { - return new Android23(openSslSocketClass, setUseSessionTickets, setHostname); + return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname); } } catch (ClassNotFoundException ignored) { // This isn't an Android runtime. @@ -158,12 +179,35 @@ public class Platform { Class serverProviderClass = Class.forName(npnClassName + "$ServerProvider"); Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass); Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class); - return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass, + return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass, serverProviderClass); } catch (ClassNotFoundException ignored) { - return new Platform(); // NPN isn't on the classpath. + // NPN isn't on the classpath. } catch (NoSuchMethodException ignored) { - return new Platform(); // The NPN version isn't what we expect. + // The NPN version isn't what we expect. + } + + return getMtu != null ? new Java5(getMtu) : new Platform(); + } + + private static class Java5 extends Platform { + private final Method getMtu; + + private Java5(Method getMtu) { + this.getMtu = getMtu; + } + + @Override public int getMtu(Socket socket) throws IOException { + try { + NetworkInterface networkInterface = NetworkInterface.getByInetAddress( + socket.getLocalAddress()); + return (Integer) getMtu.invoke(networkInterface); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IOException) throw (IOException) e.getCause(); + throw new RuntimeException(e.getCause()); + } } } @@ -171,13 +215,14 @@ public class Platform { * Android version 2.3 and newer support TLS session tickets and server name * indication (SNI). */ - private static class Android23 extends Platform { + private static class Android23 extends Java5 { protected final Class openSslSocketClass; private final Method setUseSessionTickets; private final Method setHostname; - private Android23(Class openSslSocketClass, Method setUseSessionTickets, + private Android23(Method getMtu, Class openSslSocketClass, Method setUseSessionTickets, Method setHostname) { + super(getMtu); this.openSslSocketClass = openSslSocketClass; this.setUseSessionTickets = setUseSessionTickets; this.setHostname = setHostname; @@ -204,9 +249,9 @@ public class Platform { private final Method setNpnProtocols; private final Method getNpnSelectedProtocol; - private Android41(Class openSslSocketClass, Method setUseSessionTickets, Method setHostname, - Method setNpnProtocols, Method getNpnSelectedProtocol) { - super(openSslSocketClass, setUseSessionTickets, setHostname); + private Android41(Method getMtu, Class openSslSocketClass, Method setUseSessionTickets, + Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) { + super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname); this.setNpnProtocols = setNpnProtocols; this.getNpnSelectedProtocol = getNpnSelectedProtocol; } @@ -242,14 +287,15 @@ public class Platform { * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class * path. */ - private static class JdkWithJettyNpnPlatform extends Platform { + private static class JdkWithJettyNpnPlatform extends Java5 { private final Method getMethod; private final Method putMethod; private final Class clientProviderClass; private final Class serverProviderClass; - public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class clientProviderClass, - Class serverProviderClass) { + public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod, + Class clientProviderClass, Class serverProviderClass) { + super(getMtu); this.putMethod = putMethod; this.getMethod = getMethod; this.clientProviderClass = clientProviderClass; diff --git a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java index 93f17540..3ddc693c 100644 --- a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java +++ b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java @@ -21,26 +21,23 @@ import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; -import static com.squareup.okhttp.internal.Util.ISO_8859_1; -import static com.squareup.okhttp.internal.Util.US_ASCII; -import static com.squareup.okhttp.internal.Util.UTF_8; - /** * Buffers input from an {@link InputStream} for reading lines. * - * This class is used for buffered reading of lines. For purposes of this class, a line ends with + *

This class is used for buffered reading of lines. For purposes of this class, a line ends with * "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()} * to detect it after catching the {@code EOFException}. * - * This class is intended for reading input that strictly consists of lines, such as line-based - * cache entries or cache journal. Unlike the {@link BufferedReader} which in conjunction with - * {@link InputStreamReader} provides similar functionality, this class uses different + *

This class is intended for reading input that strictly consists of lines, such as line-based + * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction + * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different * end-of-input reporting and a more restrictive definition of a line. * - * This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 + *

This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 * and 10, respectively, and the representation of no other character contains these values. * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. * The default charset is US_ASCII. @@ -52,42 +49,22 @@ public class StrictLineReader implements Closeable { private final InputStream in; private final Charset charset; - // Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end - // and the data in the range [pos, end) is buffered for reading. At end of input, if there is - // an unterminated line, we set end == -1, otherwise end == pos. If the underlying - // {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. + /* + * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end + * and the data in the range [pos, end) is buffered for reading. At end of input, if there is + * an unterminated line, we set end == -1, otherwise end == pos. If the underlying + * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. + */ private byte[] buf; private int pos; private int end; - /** - * Constructs a new {@code StrictLineReader} with the default capacity and charset. - * - * @param in the {@code InputStream} to read data from. - * @throws NullPointerException if {@code in} is null. - */ - public StrictLineReader(InputStream in) { - this(in, 8192); - } - - /** - * Constructs a new {@code LineReader} with the specified capacity and the default charset. - * - * @param in the {@code InputStream} to read data from. - * @param capacity the capacity of the buffer. - * @throws NullPointerException if {@code in} is null. - * @throws IllegalArgumentException for negative or zero {@code capacity}. - */ - public StrictLineReader(InputStream in, int capacity) { - this(in, capacity, US_ASCII); - } - /** * Constructs a new {@code LineReader} with the specified charset and the default capacity. * * @param in the {@code InputStream} to read data from. - * @param charset the charset used to decode data. - * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. * @throws NullPointerException if {@code in} or {@code charset} is null. * @throws IllegalArgumentException if the specified charset is not supported. */ @@ -100,11 +77,11 @@ public class StrictLineReader implements Closeable { * * @param in the {@code InputStream} to read data from. * @param capacity the capacity of the buffer. - * @param charset the charset used to decode data. - * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. * @throws NullPointerException if {@code in} or {@code charset} is null. * @throws IllegalArgumentException if {@code capacity} is negative or zero - * or the specified charset is not supported. + * or the specified charset is not supported. */ public StrictLineReader(InputStream in, int capacity, Charset charset) { if (in == null || charset == null) { @@ -113,7 +90,7 @@ public class StrictLineReader implements Closeable { if (capacity < 0) { throw new IllegalArgumentException("capacity <= 0"); } - if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) { + if (!(charset.equals(Util.US_ASCII))) { throw new IllegalArgumentException("Unsupported encoding"); } @@ -128,7 +105,6 @@ public class StrictLineReader implements Closeable { * * @throws IOException for errors when closing the underlying {@code InputStream}. */ - @Override public void close() throws IOException { synchronized (in) { if (buf != null) { @@ -162,7 +138,7 @@ public class StrictLineReader implements Closeable { for (int i = pos; i != end; ++i) { if (buf[i] == LF) { int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; - String res = new String(buf, pos, lineEnd - pos, charset); + String res = new String(buf, pos, lineEnd - pos, charset.name()); pos = i + 1; return res; } @@ -173,7 +149,11 @@ public class StrictLineReader implements Closeable { @Override public String toString() { int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; - return new String(buf, 0, length, charset); + try { + return new String(buf, 0, length, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // Since we control the charset this will never happen. + } } }; @@ -215,9 +195,6 @@ public class StrictLineReader implements Closeable { /** * Reads new input data into the buffer. Call only with pos == end or end == -1, * depending on the desired outcome if the function throws. - * - * @throws IOException for underlying {@code InputStream} errors. - * @throws EOFException for the end of source stream. */ private void fillBuf() throws IOException { int result = in.read(buf, 0, buf.length); diff --git a/framework/src/com/squareup/okhttp/internal/Util.java b/framework/src/com/squareup/okhttp/internal/Util.java index dc914ccd..290e5ea9 100644 --- a/framework/src/com/squareup/okhttp/internal/Util.java +++ b/framework/src/com/squareup/okhttp/internal/Util.java @@ -149,12 +149,14 @@ public final class Util { throw new AssertionError(thrown); } - /** Recursively delete everything in {@code dir}. */ - // TODO: this should specify paths as Strings rather than as Files + /** + * Deletes the contents of {@code dir}. Throws an IOException if any file + * could not be deleted, or if {@code dir} is not a readable directory. + */ public static void deleteContents(File dir) throws IOException { File[] files = dir.listFiles(); if (files == null) { - throw new IllegalArgumentException("not a directory: " + dir); + throw new IOException("not a readable directory: " + dir); } for (File file : files) { if (file.isDirectory()) { diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java index 9caeb196..7a06dca5 100644 --- a/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java @@ -19,7 +19,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; -import com.squareup.okhttp.OkResponseCache; import com.squareup.okhttp.ResponseSource; import com.squareup.okhttp.TunnelRequest; import com.squareup.okhttp.internal.Dns; @@ -154,7 +153,7 @@ public class HttpEngine { try { uri = Platform.get().toUriLenient(policy.getURL()); } catch (URISyntaxException e) { - throw new IOException(e); + throw new IOException(e.getMessage()); } this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders)); @@ -176,8 +175,8 @@ public class HttpEngine { prepareRawRequestHeaders(); initResponseSource(); - if (policy.responseCache instanceof OkResponseCache) { - ((OkResponseCache) policy.responseCache).trackResponse(responseSource); + if (policy.responseCache != null) { + policy.responseCache.trackResponse(responseSource); } // The raw response source may require the network, but the request @@ -198,6 +197,7 @@ public class HttpEngine { sendSocketRequest(); } else if (connection != null) { policy.connectionPool.recycle(connection); + policy.getFailedRoutes().remove(connection.getRoute()); connection = null; } } @@ -279,16 +279,17 @@ public class HttpEngine { } Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory, hostnameVerifier, policy.requestedProxy); - routeSelector = - new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT); + routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, + Dns.DEFAULT, policy.getFailedRoutes()); } connection = routeSelector.next(); if (!connection.isConnected()) { connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig()); policy.connectionPool.maybeShare(connection); + policy.getFailedRoutes().remove(connection.getRoute()); } connected(connection); - if (connection.getProxy() != policy.requestedProxy) { + if (connection.getRoute().getProxy() != policy.requestedProxy) { // Update the request line if the proxy changed; it may need a host name. requestHeaders.getHeaders().setRequestLine(getRequestLine()); } @@ -574,7 +575,7 @@ public class HttpEngine { protected boolean includeAuthorityInRequestLine() { return connection == null ? policy.usingProxy() // A proxy was requested. - : connection.getProxy().type() == Proxy.Type.HTTP; // A proxy was selected. + : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected. } public static String getDefaultUserAgent() { @@ -635,11 +636,8 @@ public class HttpEngine { release(false); ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); setResponse(combinedHeaders, cachedResponseBody); - if (policy.responseCache instanceof OkResponseCache) { - OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache; - httpResponseCache.trackConditionalCacheHit(); - httpResponseCache.update(cacheResponse, policy.getHttpConnectionToCache()); - } + policy.responseCache.trackConditionalCacheHit(); + policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache()); return; } else { Util.closeQuietly(cachedResponseBody); diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java index dd7a38dc..f6d77b25 100644 --- a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java @@ -17,8 +17,8 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; +import com.squareup.okhttp.internal.AbstractOutputStream; import com.squareup.okhttp.internal.Util; -import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -30,14 +30,6 @@ import java.net.Socket; import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; public final class HttpTransport implements Transport { - /** - * The maximum number of bytes to buffer when sending headers and a request - * body. When the headers and body can be sent in a single write, the - * request completes sooner. In one WiFi benchmark, using a large enough - * buffer sped up some uploads by half. - */ - private static final int MAX_REQUEST_BUFFER_LENGTH = 32768; - /** * The timeout to use while discarding a stream of input data. Since this is * used for connection reuse, this timeout should be significantly less than @@ -129,14 +121,8 @@ public final class HttpTransport implements Transport { */ public void writeRequestHeaders() throws IOException { httpEngine.writingRequestHeaders(); - int contentLength = httpEngine.requestHeaders.getContentLength(); RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders(); byte[] bytes = headersToSend.toBytes(); - - if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) { - requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength); - } - requestOut.write(bytes); } @@ -154,7 +140,7 @@ public final class HttpTransport implements Transport { } // We cannot reuse sockets that have incomplete output. - if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) { + if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) { return false; } @@ -224,7 +210,7 @@ public final class HttpTransport implements Transport { } /** An HTTP body with a fixed length known in advance. */ - private static final class FixedLengthOutputStream extends AbstractHttpOutputStream { + private static final class FixedLengthOutputStream extends AbstractOutputStream { private final OutputStream socketOut; private int bytesRemaining; @@ -266,7 +252,7 @@ public final class HttpTransport implements Transport { * buffered until {@code maxChunkLength} bytes are ready, at which point the * chunk is written and the buffer is cleared. */ - private static final class ChunkedOutputStream extends AbstractHttpOutputStream { + private static final class ChunkedOutputStream extends AbstractOutputStream { private static final byte[] CRLF = { '\r', '\n' }; private static final byte[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java index 25e8c3c8..eabe649d 100644 --- a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -20,6 +20,9 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Route; +import com.squareup.okhttp.internal.AbstractOutputStream; +import com.squareup.okhttp.internal.FaultRecoveringOutputStream; import com.squareup.okhttp.internal.Util; import java.io.FileNotFoundException; import java.io.IOException; @@ -32,13 +35,13 @@ import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.Proxy; import java.net.ProxySelector; -import java.net.ResponseCache; import java.net.SocketPermission; import java.net.URL; import java.security.Permission; import java.security.cert.CertificateException; import java.util.List; import java.util.Map; +import java.util.Set; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocketFactory; @@ -70,6 +73,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection { */ private static final int MAX_REDIRECTS = 20; + /** + * The minimum number of request body bytes to transmit before we're willing + * to let a routine {@link IOException} bubble up to the user. This is used to + * size a buffer for data that will be replayed upon error. + */ + private static final int MAX_REPLAY_BUFFER_LENGTH = 8192; + private final boolean followProtocolRedirects; /** The proxy requested by the client, or null for a proxy to be selected automatically. */ @@ -77,29 +87,37 @@ public class HttpURLConnectionImpl extends HttpURLConnection { final ProxySelector proxySelector; final CookieHandler cookieHandler; - final ResponseCache responseCache; + final OkResponseCache responseCache; final ConnectionPool connectionPool; /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */ SSLSocketFactory sslSocketFactory; HostnameVerifier hostnameVerifier; + final Set failedRoutes; private final RawHeaders rawRequestHeaders = new RawHeaders(); private int redirectionCount; + private FaultRecoveringOutputStream faultRecoveringRequestBody; protected IOException httpEngineFailure; protected HttpEngine httpEngine; - public HttpURLConnectionImpl(URL url, OkHttpClient client) { + public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache, + Set failedRoutes) { super(url); this.followProtocolRedirects = client.getFollowProtocolRedirects(); + this.failedRoutes = failedRoutes; this.requestedProxy = client.getProxy(); this.proxySelector = client.getProxySelector(); this.cookieHandler = client.getCookieHandler(); - this.responseCache = client.getResponseCache(); this.connectionPool = client.getConnectionPool(); this.sslSocketFactory = client.getSslSocketFactory(); this.hostnameVerifier = client.getHostnameVerifier(); + this.responseCache = responseCache; + } + + Set getFailedRoutes() { + return failedRoutes; } @Override public final void connect() throws IOException { @@ -216,14 +234,29 @@ public class HttpURLConnectionImpl extends HttpURLConnection { @Override public final OutputStream getOutputStream() throws IOException { connect(); - OutputStream result = httpEngine.getRequestBody(); - if (result == null) { + OutputStream out = httpEngine.getRequestBody(); + if (out == null) { throw new ProtocolException("method does not support a request body: " + method); } else if (httpEngine.hasResponse()) { throw new ProtocolException("cannot write request body after response has been read"); } - return result; + if (faultRecoveringRequestBody == null) { + faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) { + @Override protected OutputStream replacementStream(IOException e) throws IOException { + if (httpEngine.getRequestBody() instanceof AbstractOutputStream + && ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) { + return null; // Don't recover once the underlying stream has been closed. + } + if (handleFailure(e)) { + return httpEngine.getRequestBody(); + } + return null; // This is a permanent failure. + } + }; + } + + return faultRecoveringRequestBody; } @Override public final Permission getPermission() throws IOException { @@ -353,29 +386,50 @@ public class HttpURLConnectionImpl extends HttpURLConnection { } return true; } catch (IOException e) { - RouteSelector routeSelector = httpEngine.routeSelector; - if (routeSelector != null && httpEngine.connection != null) { - routeSelector.connectFailed(httpEngine.connection, e); - } - if (routeSelector == null && httpEngine.connection == null) { - throw e; // If we failed before finding a route or a connection, give up. - } - - // The connection failure isn't fatal if there's another route to attempt. - OutputStream requestBody = httpEngine.getRequestBody(); - if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody - == null || requestBody instanceof RetryableOutputStream)) { - httpEngine.release(true); - httpEngine = - newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody); - httpEngine.routeSelector = routeSelector; // Keep the same routeSelector. + if (handleFailure(e)) { return false; + } else { + throw e; } - httpEngineFailure = e; - throw e; } } + /** + * Report and attempt to recover from {@code e}. Returns true if the HTTP + * engine was replaced and the request should be retried. Otherwise the + * failure is permanent. + */ + private boolean handleFailure(IOException e) throws IOException { + RouteSelector routeSelector = httpEngine.routeSelector; + if (routeSelector != null && httpEngine.connection != null) { + routeSelector.connectFailed(httpEngine.connection, e); + } + + OutputStream requestBody = httpEngine.getRequestBody(); + boolean canRetryRequestBody = requestBody == null + || requestBody instanceof RetryableOutputStream + || (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()); + if (routeSelector == null && httpEngine.connection == null // No connection. + || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt. + || !isRecoverable(e) + || !canRetryRequestBody) { + httpEngineFailure = e; + return false; + } + + httpEngine.release(true); + RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream + ? (RetryableOutputStream) requestBody + : null; + httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream); + httpEngine.routeSelector = routeSelector; // Keep the same routeSelector. + if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) { + httpEngine.sendRequest(); + faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody()); + } + return true; + } + private boolean isRecoverable(IOException e) { // If the problem was a CertificateException from the X509TrustManager, // do not retry, we didn't have an abrupt server initiated exception. @@ -385,7 +439,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { return !sslFailure && !protocolFailure; } - HttpEngine getHttpEngine() { + public HttpEngine getHttpEngine() { return httpEngine; } @@ -402,7 +456,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { */ private Retry processResponseHeaders() throws IOException { Proxy selectedProxy = httpEngine.connection != null - ? httpEngine.connection.getProxy() + ? httpEngine.connection.getRoute().getProxy() : requestedProxy; final int responseCode = getResponseCode(); switch (responseCode) { diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java index c224270b..235f8629 100644 --- a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java @@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Route; import com.squareup.okhttp.TunnelRequest; import java.io.IOException; import java.io.InputStream; @@ -32,6 +33,7 @@ import java.security.Principal; import java.security.cert.Certificate; import java.util.List; import java.util.Map; +import java.util.Set; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLPeerUnverifiedException; @@ -45,9 +47,10 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection { /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */ private final HttpUrlConnectionDelegate delegate; - public HttpsURLConnectionImpl(URL url, OkHttpClient client) { + public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache, + Set failedRoutes) { super(url); - delegate = new HttpUrlConnectionDelegate(url, client); + delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes); } @Override public String getCipherSuite() { @@ -112,7 +115,7 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection { return null; } - HttpEngine getHttpEngine() { + public HttpEngine getHttpEngine() { return delegate.getHttpEngine(); } @@ -399,8 +402,9 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection { } private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { - private HttpUrlConnectionDelegate(URL url, OkHttpClient client) { - super(url, client); + private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache, + Set failedRoutes) { + super(url, client, responseCache, failedRoutes); } @Override protected HttpURLConnection getHttpConnectionToCache() { @@ -425,8 +429,7 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection { * @param policy the HttpURLConnectionImpl with connection configuration */ public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, - Connection connection, RetryableOutputStream requestBody) - throws IOException { + Connection connection, RetryableOutputStream requestBody) throws IOException { super(policy, method, requestHeaders, connection, requestBody); this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null; } diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java new file mode 100644 index 00000000..5829f024 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * 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.internal.http; + +import com.squareup.okhttp.ResponseSource; +import java.io.IOException; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URLConnection; +import java.util.List; +import java.util.Map; + +/** + * An extended response cache API. Unlike {@link java.net.ResponseCache}, this + * interface supports conditional caching and statistics. + * + *

Along with the rest of the {@code internal} package, this is not a public + * API. Applications wishing to supply their own caches must use the more + * limited {@link java.net.ResponseCache} interface. + */ +public interface OkResponseCache { + CacheResponse get(URI uri, String requestMethod, Map> requestHeaders) + throws IOException; + + CacheRequest put(URI uri, URLConnection urlConnection) throws IOException; + + /** + * Handles a conditional request hit by updating the stored cache response + * with the headers from {@code httpConnection}. The cached response body is + * not updated. If the stored response has changed since {@code + * conditionalCacheHit} was returned, this does nothing. + */ + void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException; + + /** Track an conditional GET that was satisfied by this cache. */ + void trackConditionalCacheHit(); + + /** Track an HTTP response being satisfied by {@code source}. */ + void trackResponse(ResponseSource source); +} diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java new file mode 100644 index 00000000..2ac915a8 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * 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.internal.http; + +import com.squareup.okhttp.ResponseSource; +import java.io.IOException; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.HttpURLConnection; +import java.net.ResponseCache; +import java.net.URI; +import java.net.URLConnection; +import java.util.List; +import java.util.Map; + +public final class OkResponseCacheAdapter implements OkResponseCache { + private final ResponseCache responseCache; + public OkResponseCacheAdapter(ResponseCache responseCache) { + this.responseCache = responseCache; + } + + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + return responseCache.get(uri, requestMethod, requestHeaders); + } + + @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { + return responseCache.put(uri, urlConnection); + } + + @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) + throws IOException { + } + + @Override public void trackConditionalCacheHit() { + } + + @Override public void trackResponse(ResponseSource source) { + } +} diff --git a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java index c121abce..eba887ec 100644 --- a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java +++ b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java @@ -17,7 +17,6 @@ package com.squareup.okhttp.internal.http; -import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.Util; import java.io.IOException; import java.io.InputStream; @@ -186,25 +185,27 @@ public final class RawHeaders { public void addLine(String line) { int index = line.indexOf(":"); if (index == -1) { - add("", line); + addLenient("", line); } else { - add(line.substring(0, index), line.substring(index + 1)); + addLenient(line.substring(0, index), line.substring(index + 1)); } } /** Add a field with the specified value. */ public void add(String fieldName, String value) { - if (fieldName == null) { - throw new IllegalArgumentException("fieldName == null"); - } - if (value == null) { - // Given null values, the RI sends a malformed field line like - // "Accept\r\n". For platform compatibility and HTTP compliance, we - // print a warning and ignore null values. - Platform.get() - .logW("Ignoring HTTP header field '" + fieldName + "' because its value is null"); - return; + if (fieldName == null) throw new IllegalArgumentException("fieldname == null"); + if (value == null) throw new IllegalArgumentException("value == null"); + if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value); } + addLenient(fieldName, value); + } + + /** + * Add a field with the specified value without any validation. Only + * appropriate for headers from the remote peer. + */ + private void addLenient(String fieldName, String value) { namesAndValues.add(fieldName); namesAndValues.add(value.trim()); } @@ -351,7 +352,9 @@ public final class RawHeaders { String fieldName = entry.getKey(); List values = entry.getValue(); if (fieldName != null) { - result.addAll(fieldName, values); + for (String value : values) { + result.addLenient(fieldName, value); + } } else if (!values.isEmpty()) { result.setStatusLine(values.get(values.size() - 1)); } @@ -371,14 +374,6 @@ public final class RawHeaders { String name = namesAndValues.get(i).toLowerCase(Locale.US); String value = namesAndValues.get(i + 1); - // TODO: promote this check to where names and values are created - if (name.length() == 0 - || value.length() == 0 - || name.indexOf('\0') != -1 - || value.indexOf('\0') != -1) { - throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); - } - // Drop headers that are forbidden when layering HTTP over SPDY. if (name.equals("connection") || name.equals("host") diff --git a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java index 2544ceed..5ec4fcca 100644 --- a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java +++ b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; /** Parsed HTTP request headers. */ -final class RequestHeaders { +public final class RequestHeaders { private final URI uri; private final RawHeaders headers; diff --git a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java index 22d8c5c3..2ab564dc 100644 --- a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java +++ b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java @@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit; import static com.squareup.okhttp.internal.Util.equal; /** Parsed HTTP response headers. */ -final class ResponseHeaders { +public final class ResponseHeaders { /** HTTP header name for the local time when the request was sent. */ private static final String SENT_MILLIS = "X-Android-Sent-Millis"; @@ -410,7 +410,8 @@ final class ResponseHeaders { if (ageMillis + minFreshMillis >= freshMillis) { headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); } - if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) { + long oneDayMillis = 24 * 60 * 60 * 1000L; + if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); } return ResponseSource.CACHE; diff --git a/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java index 325327db..5eb6b764 100644 --- a/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java +++ b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java @@ -16,6 +16,7 @@ package com.squareup.okhttp.internal.http; +import com.squareup.okhttp.internal.AbstractOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -28,7 +29,7 @@ import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; * the post body to be transparently re-sent if the HTTP request must be * sent multiple times. */ -final class RetryableOutputStream extends AbstractHttpOutputStream { +final class RetryableOutputStream extends AbstractOutputStream { private final int limit; private final ByteArrayOutputStream content; diff --git a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java index 798cff3b..ce0a71d8 100644 --- a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java +++ b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java @@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; +import com.squareup.okhttp.Route; import com.squareup.okhttp.internal.Dns; import java.io.IOException; import java.net.InetAddress; @@ -28,8 +29,11 @@ import java.net.SocketAddress; import java.net.URI; import java.net.UnknownHostException; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; +import java.util.Set; +import javax.net.ssl.SSLHandshakeException; import static com.squareup.okhttp.internal.Util.getEffectivePort; @@ -51,6 +55,7 @@ public final class RouteSelector { private final ProxySelector proxySelector; private final ConnectionPool pool; private final Dns dns; + private final Set failedRoutes; /* The most recently attempted route. */ private Proxy lastProxy; @@ -64,19 +69,23 @@ public final class RouteSelector { /* State for negotiating the next InetSocketAddress to use. */ private InetAddress[] socketAddresses; private int nextSocketAddressIndex; - private String socketHost; private int socketPort; /* State for negotiating the next TLS configuration */ private int nextTlsMode = TLS_MODE_NULL; + /* State for negotiating failed routes */ + private final List postponedRoutes; + public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool, - Dns dns) { + Dns dns, Set failedRoutes) { this.address = address; this.uri = uri; this.proxySelector = proxySelector; this.pool = pool; this.dns = dns; + this.failedRoutes = failedRoutes; + this.postponedRoutes = new LinkedList(); resetNextProxy(uri, address.getProxy()); } @@ -86,7 +95,7 @@ public final class RouteSelector { * least one route. */ public boolean hasNext() { - return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy(); + return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed(); } /** @@ -105,7 +114,10 @@ public final class RouteSelector { if (!hasNextTlsMode()) { if (!hasNextInetSocketAddress()) { if (!hasNextProxy()) { - throw new NoSuchElementException(); + if (!hasNextPostponed()) { + throw new NoSuchElementException(); + } + return new Connection(nextPostponed()); } lastProxy = nextProxy(); resetNextInetSocketAddress(lastProxy); @@ -113,9 +125,17 @@ public final class RouteSelector { lastInetSocketAddress = nextInetSocketAddress(); resetNextTlsMode(); } - boolean modernTls = nextTlsMode() == TLS_MODE_MODERN; - return new Connection(address, lastProxy, lastInetSocketAddress, modernTls); + boolean modernTls = nextTlsMode() == TLS_MODE_MODERN; + Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls); + if (failedRoutes.contains(route)) { + postponedRoutes.add(route); + // We will only recurse in order to skip previously failed routes. They will be + // tried last. + return next(); + } + + return new Connection(route); } /** @@ -123,9 +143,17 @@ public final class RouteSelector { * failure on a connection returned by this route selector. */ public void connectFailed(Connection connection, IOException failure) { - if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) { + Route failedRoute = connection.getRoute(); + if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) { // Tell the proxy selector when we fail to connect on a fresh connection. - proxySelector.connectFailed(uri, connection.getProxy().address(), failure); + proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure); + } + + failedRoutes.add(failedRoute); + if (!(failure instanceof SSLHandshakeException)) { + // If the problem was not related to SSL then it will also fail with + // a different Tls mode therefore we can be proactive about it. + failedRoutes.add(failedRoute.flipTlsMode()); } } @@ -175,6 +203,7 @@ public final class RouteSelector { private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException { socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws! + String socketHost; if (proxy.type() == Proxy.Type.DIRECT) { socketHost = uri.getHost(); socketPort = getEffectivePort(uri); @@ -233,4 +262,14 @@ public final class RouteSelector { throw new AssertionError(); } } + + /** Returns true if there is another postponed route to try. */ + private boolean hasNextPostponed() { + return !postponedRoutes.isEmpty(); + } + + /** Returns the next postponed route to try. */ + private Route nextPostponed() { + return postponedRoutes.remove(0); + } } diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java index b3e248c3..fccd14f8 100644 --- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java +++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java @@ -139,17 +139,17 @@ public final class SpdyConnection implements Closeable { return stream; } - private void setIdle(boolean value) { + private synchronized void setIdle(boolean value) { idleStartTimeNs = value ? System.nanoTime() : 0L; } /** Returns true if this connection is idle. */ - public boolean isIdle() { + public synchronized boolean isIdle() { return idleStartTimeNs != 0L; } /** Returns the time in ns when this connection became idle or 0L if connection is not idle. */ - public long getIdleStartTimeNs() { + public synchronized long getIdleStartTimeNs() { return idleStartTimeNs; } diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java index 7a7b1987..7d3f2bd5 100644 --- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java +++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java @@ -21,6 +21,7 @@ import java.io.Closeable; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.ProtocolException; import java.util.ArrayList; import java.util.List; @@ -31,39 +32,46 @@ import java.util.zip.InflaterInputStream; /** Read spdy/3 frames. */ final class SpdyReader implements Closeable { - static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" - + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" - + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" - + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" - + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" - + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" - + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" - + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" - + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" - + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" - + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" - + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" - + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" - + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" - + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" - + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" - + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" - + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" - + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" - + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" - + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" - + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" - + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" - + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" - + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" - + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" - + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori" - + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" - + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" - + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" - + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" - + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," - + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8); + static final byte[] DICTIONARY; + static { + try { + DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" + + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" + + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" + + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" + + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" + + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" + + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" + + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" + + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" + + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" + + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" + + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" + + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" + + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" + + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" + + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" + + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" + + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" + + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" + + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" + + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" + + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" + + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" + + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" + + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" + + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" + + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori" + + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" + + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" + + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" + + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" + + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," + + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } private final DataInputStream in; private final DataInputStream nameValueBlockIn; @@ -252,7 +260,7 @@ final class SpdyReader implements Closeable { return entries; } catch (DataFormatException e) { - throw new IOException(e); + throw new IOException(e.getMessage()); } }