Update okhttp to commit b48247968a.

Includes mechanism to turn of SPDY on a per request
basis. Leaves SPDY turned off by default.

Change-Id: Ie7a0c7ebfd37d5d2653266e7b6925a2f346323ad
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
index 1dc3826..515725a 100644
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpsHandler.java
@@ -22,8 +22,12 @@
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.URLStreamHandler;
+import java.util.Arrays;
+import java.util.List;
 
 public final class HttpsHandler extends URLStreamHandler {
+    private static final List<String> ENABLED_TRANSPORTS = Arrays.asList("http/1.1");
+
     @Override protected URLConnection openConnection(URL url) throws IOException {
         return new OkHttpClient().open(url);
     }
@@ -32,7 +36,7 @@
         if (url == null || proxy == null) {
             throw new IllegalArgumentException("url == null || proxy == null");
         }
-        return new OkHttpClient().setProxy(proxy).open(url);
+        return new OkHttpClient().setProxy(proxy).setTransports(ENABLED_TRANSPORTS).open(url);
     }
 
     @Override protected int getDefaultPort() {
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index fe0745c..db633c6 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -18,6 +18,7 @@
 
 import dalvik.system.SocketTagger;
 import java.io.OutputStream;
+import java.net.NetworkInterface;
 import java.net.Socket;
 import java.net.SocketException;
 import java.net.URI;
@@ -34,6 +35,12 @@
 public final class Platform {
     private static final Platform PLATFORM = new Platform();
 
+    /*
+     * Default for the maximum transmission unit, used only if
+     * there's an error retrieving it via NetworkInterface.
+     */
+    private static final int DEFAULT_MTU = 1400;
+
     public static Platform get() {
         return PLATFORM;
     }
@@ -94,4 +101,26 @@
             OutputStream out, Deflater deflater, boolean syncFlush) {
         return new DeflaterOutputStream(out, deflater, syncFlush);
     }
+
+    /**
+     * Returns the maximum transmission unit of the network interface used by
+     * {@code socket}, or a reasonable default if there's an error retrieving
+     * it from the socket.
+     *
+     * <p>The returned value should only be used as an optimization; such as to
+     * size buffers efficiently.
+     */
+    public int getMtu(Socket socket) {
+        try {
+            NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+                socket.getLocalAddress());
+            if (networkInterface != null) {
+                return networkInterface.getMTU();
+            }
+
+            return DEFAULT_MTU;
+        } catch (SocketException exception) {
+            return DEFAULT_MTU;
+        }
+    }
 }
diff --git a/src/main/java/com/squareup/okhttp/Address.java b/src/main/java/com/squareup/okhttp/Address.java
index cd41ac9..b34bd91 100644
--- a/src/main/java/com/squareup/okhttp/Address.java
+++ b/src/main/java/com/squareup/okhttp/Address.java
@@ -15,8 +15,10 @@
  */
 package com.squareup.okhttp;
 
+import com.squareup.okhttp.internal.Util;
 import java.net.Proxy;
 import java.net.UnknownHostException;
+import java.util.List;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLSocketFactory;
 
@@ -38,16 +40,23 @@
   final int uriPort;
   final SSLSocketFactory sslSocketFactory;
   final HostnameVerifier hostnameVerifier;
+  final OkAuthenticator authenticator;
+  final List<String> transports;
 
   public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
-      HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
+      HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy,
+      List<String> transports) throws UnknownHostException {
     if (uriHost == null) throw new NullPointerException("uriHost == null");
     if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
+    if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
+    if (transports == null) throw new IllegalArgumentException("transports == null");
     this.proxy = proxy;
     this.uriHost = uriHost;
     this.uriPort = uriPort;
     this.sslSocketFactory = sslSocketFactory;
     this.hostnameVerifier = hostnameVerifier;
+    this.authenticator = authenticator;
+    this.transports = Util.immutableList(transports);
   }
 
   /** Returns the hostname of the origin server. */
@@ -79,6 +88,22 @@
     return hostnameVerifier;
   }
 
+
+  /**
+   * Returns the client's authenticator. This method never returns null.
+   */
+  public OkAuthenticator getAuthenticator() {
+    return authenticator;
+  }
+
+  /**
+   * Returns the client's transports. This method always returns a non-null list
+   * that contains "http/1.1", possibly among other transports.
+   */
+  public List<String> getTransports() {
+    return transports;
+  }
+
   /**
    * Returns this address's explicitly-specified HTTP proxy, or null to
    * delegate to the HTTP client's proxy selector.
@@ -94,7 +119,9 @@
           && this.uriHost.equals(that.uriHost)
           && this.uriPort == that.uriPort
           && equal(this.sslSocketFactory, that.sslSocketFactory)
-          && equal(this.hostnameVerifier, that.hostnameVerifier);
+          && equal(this.hostnameVerifier, that.hostnameVerifier)
+          && equal(this.authenticator, that.authenticator)
+          && equal(this.transports, that.transports);
     }
     return false;
   }
@@ -105,7 +132,9 @@
     result = 31 * result + uriPort;
     result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
     result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+    result = 31 * result + (authenticator != null ? authenticator.hashCode() : 0);
     result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+    result = 31 * result + transports.hashCode();
     return result;
   }
 }
diff --git a/src/main/java/com/squareup/okhttp/Connection.java b/src/main/java/com/squareup/okhttp/Connection.java
index 2394b77..5ec99cc 100644
--- a/src/main/java/com/squareup/okhttp/Connection.java
+++ b/src/main/java/com/squareup/okhttp/Connection.java
@@ -24,11 +24,11 @@
 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 @@
       '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 @@
   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,22 @@
       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);
+    if (mtu < 1024) mtu = 1024;
+    if (mtu > 8192) mtu = 8192;
+    in = new BufferedInputStream(in, mtu);
+    out = new BufferedOutputStream(out, mtu);
   }
 
   /**
@@ -136,17 +127,17 @@
     }
 
     // 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);
     }
 
-    final boolean spdyEnabled = false;
-    if (modernTls && spdyEnabled) {
+    boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
+    if (useNpn) {
       platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
     }
 
@@ -154,18 +145,19 @@
     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 && spdyEnabled && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+    if (useNpn && (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"));
@@ -182,29 +174,9 @@
     socket.close();
   }
 
-  /**
-   * Returns the proxy that this connection is using.
-   *
-   * <strong>Warning:</strong> 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;
   }
 
   /**
@@ -285,7 +257,7 @@
    * 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;
   }
 
   /**
@@ -305,9 +277,9 @@
         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(
+              route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
+              route.proxy, url);
           if (credentialsFound) {
             continue;
           } else {
diff --git a/src/main/java/com/squareup/okhttp/ConnectionPool.java b/src/main/java/com/squareup/okhttp/ConnectionPool.java
index 438fe91..009f025 100644
--- a/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -1,10 +1,25 @@
+/*
+ *  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;
 import com.squareup.okhttp.internal.Util;
 import java.net.SocketException;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -65,8 +80,9 @@
   private final LinkedList<Connection> connections = new LinkedList<Connection>();
 
   /** We use a single background thread to cleanup expired connections. */
-  private final ExecutorService executorService =
-      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+  private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
+      60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+      Util.daemonThreadFactory("OkHttp ConnectionPool"));
   private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
     @Override public Void call() throws Exception {
       List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
@@ -162,9 +178,10 @@
   /** Returns a recycled connection to {@code address}, or null if no such connection exists. */
   public synchronized Connection get(Address address) {
     Connection foundConnection = null;
-    for (Iterator<Connection> i = connections.descendingIterator(); i.hasNext(); ) {
-      Connection connection = i.next();
-      if (!connection.getAddress().equals(address)
+    for (ListIterator<Connection> i = connections.listIterator(connections.size());
+        i.hasPrevious(); ) {
+      Connection connection = i.previous();
+      if (!connection.getRoute().getAddress().equals(address)
           || !connection.isAlive()
           || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
         continue;
@@ -241,4 +258,17 @@
       }
     }
   }
+
+  /** Close and remove all connections in the pool. */
+  public void evictAll() {
+    List<Connection> connections;
+    synchronized (this) {
+      connections = new ArrayList<Connection>(this.connections);
+      this.connections.clear();
+    }
+
+    for (Connection connection : connections) {
+      Util.closeQuietly(connection);
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java b/src/main/java/com/squareup/okhttp/HttpResponseCache.java
similarity index 77%
rename from src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
rename to src/main/java/com/squareup/okhttp/HttpResponseCache.java
index 8735166..7622aa3 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/HttpResponseCache.java
@@ -14,14 +14,18 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp;
 
-import com.squareup.okhttp.OkResponseCache;
-import com.squareup.okhttp.ResponseSource;
 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;
@@ -58,11 +62,63 @@
 import static com.squareup.okhttp.internal.Util.UTF_8;
 
 /**
- * Cache responses in a directory on the file system. Most clients should use
- * {@code android.net.HttpResponseCache}, the stable, documented front end for
- * this.
+ * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
+ * saving time and bandwidth.
+ *
+ * <h3>Cache Optimization</h3>
+ * To measure cache effectiveness, this class tracks three statistics:
+ * <ul>
+ *     <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
+ *         of HTTP requests issued since this cache was created.
+ *     <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
+ *         number of those requests that required network use.
+ *     <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
+ *         those requests whose responses were served by the cache.
+ * </ul>
+ * 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.
+ *
+ * <p>The best way to improve the cache hit rate is by configuring the web
+ * server to return cacheable responses. Although this client honors all <a
+ * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
+ * headers, it doesn't cache partial responses.
+ *
+ * <h3>Force a Network Response</h3>
+ * 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: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "no-cache");
+ * }</pre>
+ * 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: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "max-age=0");
+ * }</pre>
+ *
+ * <h3>Force a Cache Response</h3>
+ * Sometimes you'll want to show resources if they are available immediately,
+ * but not otherwise. This can be used so your application can show
+ * <i>something</i> while waiting for the latest data to be downloaded. To
+ * restrict a request to locally-cached resources, add the {@code
+ * only-if-cached} directive: <pre>   {@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
+ *     }
+ * }</pre>
+ * 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: <pre>   {@code
+ *         int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
+ *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
+ * }</pre>
  */
-public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
+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' };
 
@@ -81,6 +137,36 @@
   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<String, List<String>> 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);
   }
@@ -185,13 +271,7 @@
     }
   }
 
-  /**
-   * 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.
-   */
-  @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+  private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
       throws IOException {
     HttpEngine httpEngine = getHttpEngine(httpConnection);
     URI uri = httpEngine.getUri();
@@ -234,8 +314,13 @@
     }
   }
 
-  public DiskLruCache getCache() {
-    return cache;
+  /**
+   * 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() {
@@ -246,7 +331,31 @@
     return writeSuccessCount;
   }
 
-  public synchronized void trackResponse(ResponseSource source) {
+  public long getSize() {
+    return cache.size();
+  }
+
+  public long getMaxSize() {
+    return cache.getMaxSize();
+  }
+
+  public void flush() throws IOException {
+    cache.flush();
+  }
+
+  public void close() throws IOException {
+    cache.close();
+  }
+
+  public File getDirectory() {
+    return cache.getDirectory();
+  }
+
+  public boolean isClosed() {
+    return cache.isClosed();
+  }
+
+  private synchronized void trackResponse(ResponseSource source) {
     requestCount++;
 
     switch (source) {
@@ -260,7 +369,7 @@
     }
   }
 
-  public synchronized void trackConditionalCacheHit() {
+  private synchronized void trackConditionalCacheHit() {
     hitCount++;
   }
 
@@ -405,7 +514,7 @@
 
         if (isHttps()) {
           String blank = reader.readLine();
-          if (!blank.isEmpty()) {
+          if (blank.length() > 0) {
             throw new IOException("expected \"\" but was \"" + blank + "\"");
           }
           cipherSuite = reader.readLine();
@@ -490,7 +599,7 @@
         }
         return result;
       } catch (CertificateException e) {
-        throw new IOException(e);
+        throw new IOException(e.getMessage());
       }
     }
 
@@ -507,7 +616,7 @@
           writer.write(line + '\n');
         }
       } catch (CertificateEncodingException e) {
-        throw new IOException(e);
+        throw new IOException(e.getMessage());
       }
     }
 
diff --git a/src/main/java/com/squareup/okhttp/OkAuthenticator.java b/src/main/java/com/squareup/okhttp/OkAuthenticator.java
new file mode 100644
index 0000000..a505419
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/OkAuthenticator.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.squareup.okhttp.internal.Base64;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Responds to authentication challenges from the remote web or proxy server by
+ * returning credentials.
+ */
+public interface OkAuthenticator {
+  /**
+   * Returns a credential that satisfies the authentication challenge made by
+   * {@code url}. Returns null if the challenge cannot be satisfied. This method
+   * is called in response to an HTTP 401 unauthorized status code sent by the
+   * origin server.
+   *
+   * @param challenges parsed "WWW-Authenticate" challenge headers from the HTTP
+   *     response.
+   */
+  Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
+
+  /**
+   * Returns a credential that satisfies the authentication challenge made by
+   * {@code proxy}. Returns null if the challenge cannot be satisfied. This
+   * method is called in response to an HTTP 401 unauthorized status code sent
+   * by the proxy server.
+   *
+   * @param challenges parsed "Proxy-Authenticate" challenge headers from the
+   *     HTTP response.
+   */
+  Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
+
+  /** An RFC 2617 challenge. */
+  public final class Challenge {
+    private final String scheme;
+    private final String realm;
+
+    public Challenge(String scheme, String realm) {
+      this.scheme = scheme;
+      this.realm = realm;
+    }
+
+    /** Returns the authentication scheme, like {@code Basic}. */
+    public String getScheme() {
+      return scheme;
+    }
+
+    /** Returns the protection space. */
+    public String getRealm() {
+      return realm;
+    }
+
+    @Override public boolean equals(Object o) {
+      return o instanceof Challenge
+          && ((Challenge) o).scheme.equals(scheme)
+          && ((Challenge) o).realm.equals(realm);
+    }
+
+    @Override public int hashCode() {
+      return scheme.hashCode() + 31 * realm.hashCode();
+    }
+
+    @Override public String toString() {
+      return scheme + " realm=\"" + realm + "\"";
+    }
+  }
+
+  /** An RFC 2617 credential. */
+  public final class Credential {
+    private final String headerValue;
+
+    private Credential(String headerValue) {
+      this.headerValue = headerValue;
+    }
+
+    /** Returns an auth credential for the Basic scheme. */
+    public static Credential basic(String userName, String password) {
+      try {
+        String usernameAndPassword = userName + ":" + password;
+        byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
+        String encoded = Base64.encode(bytes);
+        return new Credential("Basic " + encoded);
+      } catch (UnsupportedEncodingException e) {
+        throw new AssertionError();
+      }
+    }
+
+    public String getHeaderValue() {
+      return headerValue;
+    }
+
+    @Override public boolean equals(Object o) {
+      return o instanceof Credential && ((Credential) o).headerValue.equals(headerValue);
+    }
+
+    @Override public int hashCode() {
+      return headerValue.hashCode();
+    }
+
+    @Override public String toString() {
+      return headerValue;
+    }
+  }
+}
diff --git a/src/main/java/com/squareup/okhttp/OkHttpClient.java b/src/main/java/com/squareup/okhttp/OkHttpClient.java
index d21cdb7..da3e82c 100644
--- a/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -15,29 +15,53 @@
  */
 package com.squareup.okhttp;
 
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpAuthenticator;
 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 com.squareup.okhttp.internal.tls.OkHostnameVerifier;
 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.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
 
 /** Configures and creates HTTP connections. */
 public final class OkHttpClient {
+  private static final List<String> DEFAULT_TRANSPORTS
+      = Util.immutableList(Arrays.asList("spdy/3", "http/1.1"));
+
   private Proxy proxy;
+  private List<String> transports;
+  private final Set<Route> failedRoutes;
   private ProxySelector proxySelector;
   private CookieHandler cookieHandler;
   private ResponseCache responseCache;
   private SSLSocketFactory sslSocketFactory;
   private HostnameVerifier hostnameVerifier;
+  private OkAuthenticator authenticator;
   private ConnectionPool connectionPool;
   private boolean followProtocolRedirects = true;
 
+  public OkHttpClient() {
+    this.failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
+  }
+
+  private OkHttpClient(OkHttpClient copyFrom) {
+    this.failedRoutes = copyFrom.failedRoutes; // Avoid allocating an unnecessary LinkedHashSet.
+  }
+
   /**
    * Sets the HTTP proxy that will be used by connections created by this
    * client. This takes precedence over {@link #setProxySelector}, which is
@@ -102,13 +126,23 @@
     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.
    *
    * <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
    * system-wide default} SSL socket factory will be used.
    */
-  public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
+  public OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
     this.sslSocketFactory = sslSocketFactory;
     return this;
   }
@@ -134,6 +168,22 @@
   }
 
   /**
+   * Sets the authenticator used to respond to challenges from the remote web
+   * server or proxy server.
+   *
+   * <p>If unset, the {@link java.net.Authenticator#setDefault system-wide default}
+   * authenticator will be used.
+   */
+  public OkHttpClient setAuthenticator(OkAuthenticator authenticator) {
+    this.authenticator = authenticator;
+    return this;
+  }
+
+  public OkAuthenticator getAuthenticator() {
+    return authenticator;
+  }
+
+  /**
    * Sets the connection pool used to recycle HTTP and HTTPS connections.
    *
    * <p>If unset, the {@link ConnectionPool#getDefault() system-wide
@@ -164,23 +214,67 @@
     return followProtocolRedirects;
   }
 
+  /**
+   * Configure the transports used by this client to communicate with remote
+   * servers. By default this client will prefer the most efficient transport
+   * available, falling back to more ubiquitous transports. Applications should
+   * only call this method to avoid specific compatibility problems, such as web
+   * servers that behave incorrectly when SPDY is enabled.
+   *
+   * <p>The following transports are currently supported:
+   * <ul>
+   *   <li><a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http/1.1</a>
+   *   <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3">spdy/3</a>
+   * </ul>
+   *
+   * <p><strong>This is an evolving set.</strong> Future releases may drop
+   * support for transitional transports (like spdy/3), in favor of their
+   * successors (spdy/4 or http/2.0). The http/1.1 transport will never be
+   * dropped.
+   *
+   * <p>If multiple protocols are specified, <a
+   * href="https://technotes.googlecode.com/git/nextprotoneg.html">NPN</a> will
+   * be used to negotiate a transport. Future releases may use another mechanism
+   * (such as <a href="http://tools.ietf.org/html/draft-friedl-tls-applayerprotoneg-02">ALPN</a>)
+   * to negotiate a transport.
+   *
+   * @param transports the transports to use, in order of preference. The list
+   *     must contain "http/1.1". It must not contain null.
+   */
+  public OkHttpClient setTransports(List<String> transports) {
+    transports = Util.immutableList(transports);
+    if (!transports.contains("http/1.1")) {
+      throw new IllegalArgumentException("transports doesn't contain http/1.1: " + transports);
+    }
+    if (transports.contains(null)) {
+      throw new IllegalArgumentException("transports must not contain null");
+    }
+    this.transports = transports;
+    return this;
+  }
+
+  public List<String> getTransports() {
+    return transports;
+  }
+
   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();
+    OkHttpClient result = new OkHttpClient(this);
     result.proxy = proxy;
     result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
     result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
@@ -190,9 +284,13 @@
         : HttpsURLConnection.getDefaultSSLSocketFactory();
     result.hostnameVerifier = hostnameVerifier != null
         ? hostnameVerifier
-        : HttpsURLConnection.getDefaultHostnameVerifier();
+        : new OkHostnameVerifier();
+    result.authenticator = authenticator != null
+        ? authenticator
+        : HttpAuthenticator.SYSTEM_DEFAULT;
     result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
     result.followProtocolRedirects = followProtocolRedirects;
+    result.transports = transports != null ? transports : DEFAULT_TRANSPORTS;
     return result;
   }
 }
diff --git a/src/main/java/com/squareup/okhttp/OkResponseCache.java b/src/main/java/com/squareup/okhttp/OkResponseCache.java
deleted file mode 100644
index b7e3801..0000000
--- a/src/main/java/com/squareup/okhttp/OkResponseCache.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2012 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 java.io.IOException;
-import java.net.CacheResponse;
-import java.net.HttpURLConnection;
-
-/**
- * A response cache that supports statistics tracking and updating stored
- * responses. Implementations of {@link java.net.ResponseCache} should implement
- * this interface to receive additional support from the HTTP engine.
- */
-public interface OkResponseCache {
-
-  /** Track an HTTP response being satisfied by {@code source}. */
-  void trackResponse(ResponseSource source);
-
-  /** Track an conditional GET that was satisfied by this cache. */
-  void trackConditionalCacheHit();
-
-  /** Updates stored HTTP headers using a hit on a conditional GET. */
-  void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
-      throws IOException;
-}
diff --git a/src/main/java/com/squareup/okhttp/Route.java b/src/main/java/com/squareup/okhttp/Route.java
new file mode 100644
index 0000000..6968c60
--- /dev/null
+++ b/src/main/java/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.
+   *
+   * <strong>Warning:</strong> 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/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java b/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
similarity index 80%
rename from src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
rename to src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
index 90675b0..78c9691 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
@@ -14,18 +14,18 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal;
 
 import java.io.IOException;
 import java.io.OutputStream;
 
 /**
- * An output stream for the body of an HTTP request.
+ * An output stream for an HTTP request body.
  *
  * <p>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.
  */
-abstract class AbstractHttpOutputStream extends OutputStream {
+public abstract class AbstractOutputStream extends OutputStream {
   protected boolean closed;
 
   @Override public final void write(int data) throws IOException {
@@ -37,4 +37,9 @@
       throw new IOException("stream closed");
     }
   }
+
+  /** Returns true if this stream was closed locally. */
+  public boolean isClosed() {
+    return closed;
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
index 00fe2f1..f7fcb1e 100644
--- a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -23,7 +23,6 @@
 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.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 <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
  *
  * <p>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 @@
  * entry may have only one editor at one time; if a value is not available to be
  * edited then {@link #edit} will return null.
  * <ul>
- * <li>When an entry is being <strong>created</strong> it is necessary to
- * supply a full set of values; the empty value should be used as a
- * placeholder if necessary.
- * <li>When an entry is being <strong>edited</strong>, it is not necessary
- * to supply data for every value; values default to their previous
- * value.
+ *     <li>When an entry is being <strong>created</strong> it is necessary to
+ *         supply a full set of values; the empty value should be used as a
+ *         placeholder if necessary.
+ *     <li>When an entry is being <strong>edited</strong>, it is not necessary
+ *         to supply data for every value; values default to their previous
+ *         value.
  * </ul>
  * 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 @@
  */
 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 @@
   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<Runnable>());
   private final Callable<Void> cleanupCallable = new Callable<Void>() {
-    @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 @@
     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 @@
       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 @@
       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 @@
       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 @@
       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 @@
         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 @@
       executorService.submit(cleanupCallable);
     }
 
-    return new Snapshot(key, entry.sequenceNumber, ins);
+    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
   }
 
   /**
@@ -405,19 +452,19 @@
     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,11 +479,20 @@
    * 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
    * deletion is pending.
@@ -451,7 +507,7 @@
       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 @@
         }
         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 @@
       lruEntries.remove(entry.key);
       journalWriter.write(REMOVE + ' ' + entry.key + '\n');
     }
+    journalWriter.flush();
 
     if (size > maxSize || journalRebuildRequired()) {
       executorService.submit(cleanupCallable);
@@ -506,7 +562,8 @@
    */
   private boolean journalRebuildRequired() {
     final int redundantOpCompactThreshold = 2000;
-    return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
+    return redundantOpCount >= redundantOpCompactThreshold //
+        && redundantOpCount >= lruEntries.size();
   }
 
   /**
@@ -564,7 +621,7 @@
   /** 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<Entry>(lruEntries.values())) {
       if (entry.currentEditor != null) {
@@ -594,14 +651,14 @@
   }
 
   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 @@
     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 @@
       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 @@
         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 @@
         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 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 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 @@
       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 @@
     }
 
     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/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
new file mode 100644
index 0000000..c32b27a
--- /dev/null
+++ b/src/main/java/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.
+ *
+ * <p>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/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java b/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
index ce430b2..992b2ae 100644
--- a/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
+++ b/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
@@ -20,10 +20,10 @@
  * Runnable implementation which always sets its thread name.
  */
 public abstract class NamedRunnable implements Runnable {
-  private String name;
+  private final String name;
 
-  public NamedRunnable(String name) {
-    this.name = name;
+  public NamedRunnable(String format, Object... args) {
+    this.name = String.format(format, args);
   }
 
   @Override public final void run() {
diff --git a/src/main/java/com/squareup/okhttp/internal/Platform.java b/src/main/java/com/squareup/okhttp/internal/Platform.java
index 21761d3..c06f480 100644
--- a/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/src/main/java/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.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 @@
     }
   }
 
+  /**
+   * 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.
+   *
+   * <p>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;
@@ -145,10 +166,10 @@
       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.
@@ -165,26 +186,53 @@
       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 new Java6(getMtu);
+  }
+
+  private static class Java6 extends Platform {
+    private final Method getMtu;
+
+    private Java6(Method getMtu) {
+      this.getMtu = getMtu;
+    }
+
+    @Override public int getMtu(Socket socket) throws IOException {
+      try {
+        NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+            socket.getLocalAddress());
+        if (networkInterface == null) {
+          return super.getMtu(socket); // There's no longer an interface with this local address.
+        }
+        return (Integer) getMtu.invoke(networkInterface);
+      } catch (SocketException e) {
+        // Certain Motorola devices always throw on getByInetAddress. Return the default for those.
+        return super.getMtu(socket);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
+        throw new RuntimeException(e.getCause());
+      }
     }
   }
 
-  /**
-   * Android version 2.3 and newer support TLS session tickets and server name
-   * indication (SNI).
-   */
-  private static class Android23 extends Platform {
+  /** Android version 2.3 and newer support TLS session tickets and server name indication (SNI). */
+  private static class Android23 extends Java6 {
     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;
@@ -211,9 +259,9 @@
     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;
     }
@@ -245,18 +293,16 @@
     }
   }
 
-  /**
-   * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
-   * path.
-   */
-  private static class JdkWithJettyNpnPlatform extends Platform {
+  /** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
+  private static class JdkWithJettyNpnPlatform extends Java6 {
     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/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
index 93f1754..3ddc693 100644
--- a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
@@ -21,26 +21,23 @@
 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
+ * <p>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
+ * <p>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
+ * <p>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 @@
   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 @@
    *
    * @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 @@
     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 @@
    *
    * @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 @@
       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 @@
         @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 @@
   /**
    * 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/src/main/java/com/squareup/okhttp/internal/Util.java b/src/main/java/com/squareup/okhttp/internal/Util.java
index dc914cc..0ce7f8a 100644
--- a/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -29,6 +29,10 @@
 import java.net.URL;
 import java.nio.ByteOrder;
 import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicReference;
 
 /** Junk drawer of utility methods. */
@@ -149,12 +153,14 @@
     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()) {
@@ -322,4 +328,19 @@
     }
     return result.toString();
   }
+
+  /** Returns an immutable copy of {@code list}. */
+  public static <T> List<T> immutableList(List<T> list) {
+    return Collections.unmodifiableList(new ArrayList<T>(list));
+  }
+
+  public static ThreadFactory daemonThreadFactory(final String name) {
+    return new ThreadFactory() {
+      @Override public Thread newThread(Runnable runnable) {
+        Thread result = new Thread(runnable, name);
+        result.setDaemon(true);
+        return result;
+      }
+    };
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
index 4ccd12a..566431e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
@@ -16,7 +16,8 @@
  */
 package com.squareup.okhttp.internal.http;
 
-import com.squareup.okhttp.internal.Base64;
+import com.squareup.okhttp.OkAuthenticator;
+import com.squareup.okhttp.OkAuthenticator.Challenge;
 import java.io.IOException;
 import java.net.Authenticator;
 import java.net.InetAddress;
@@ -27,11 +28,49 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static com.squareup.okhttp.OkAuthenticator.Credential;
 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
 import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
 
 /** Handles HTTP authentication headers from origin and proxy servers. */
 public final class HttpAuthenticator {
+  /** Uses the global authenticator to get the password. */
+  public static final OkAuthenticator SYSTEM_DEFAULT = new OkAuthenticator() {
+    @Override public Credential authenticate(
+        Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
+      for (Challenge challenge : challenges) {
+        PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(url.getHost(),
+            getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
+            challenge.getRealm(), challenge.getScheme(), url, Authenticator.RequestorType.SERVER);
+        if (auth != null) {
+          return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
+        }
+      }
+      return null;
+    }
+
+    @Override public Credential authenticateProxy(
+        Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
+      for (Challenge challenge : challenges) {
+        InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+        PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
+            proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
+            url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
+            Authenticator.RequestorType.PROXY);
+        if (auth != null) {
+          return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
+        }
+      }
+      return null;
+    }
+
+    private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
+      return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
+          ? ((InetSocketAddress) proxy.address()).getAddress()
+          : InetAddress.getByName(url.getHost());
+    }
+  };
+
   private HttpAuthenticator() {
   }
 
@@ -41,68 +80,33 @@
    * @return true if credentials have been added to successorRequestHeaders
    *         and another request should be attempted.
    */
-  public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
-      RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
-    if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
-      throw new IllegalArgumentException();
+  public static boolean processAuthHeader(OkAuthenticator authenticator, int responseCode,
+      RawHeaders responseHeaders, RawHeaders successorRequestHeaders, Proxy proxy, URL url)
+      throws IOException {
+    String responseField;
+    String requestField;
+    if (responseCode == HTTP_UNAUTHORIZED) {
+      responseField = "WWW-Authenticate";
+      requestField = "Authorization";
+    } else if (responseCode == HTTP_PROXY_AUTH) {
+      responseField = "Proxy-Authenticate";
+      requestField = "Proxy-Authorization";
+    } else {
+      throw new IllegalArgumentException(); // TODO: ProtocolException?
     }
-
-    // Keep asking for username/password until authorized.
-    String challengeHeader =
-        responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate";
-    String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
-    if (credentials == null) {
-      return false; // Could not find credentials so end the request cycle.
-    }
-
-    // Add authorization credentials, bypassing the already-connected check.
-    String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization";
-    successorRequestHeaders.set(fieldName, credentials);
-    return true;
-  }
-
-  /**
-   * Returns the authorization credentials that may satisfy the challenge.
-   * Returns null if a challenge header was not provided or if credentials
-   * were not available.
-   */
-  private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
-      Proxy proxy, URL url) throws IOException {
-    List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
+    List<Challenge> challenges = parseChallenges(responseHeaders, responseField);
     if (challenges.isEmpty()) {
-      return null;
+      return false; // Could not find a challenge so end the request cycle.
     }
-
-    for (Challenge challenge : challenges) {
-      // Use the global authenticator to get the password.
-      PasswordAuthentication auth;
-      if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
-        InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
-        auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
-            getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
-            challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
-      } else {
-        auth = Authenticator.requestPasswordAuthentication(url.getHost(),
-            getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
-            challenge.scheme, url, Authenticator.RequestorType.SERVER);
-      }
-      if (auth == null) {
-        continue;
-      }
-
-      // Use base64 to encode the username and password.
-      String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
-      byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
-      String encoded = Base64.encode(bytes);
-      return challenge.scheme + " " + encoded;
+    Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH
+        ? authenticator.authenticateProxy(proxy, url, challenges)
+        : authenticator.authenticate(proxy, url, challenges);
+    if (credential == null) {
+      return false; // Could not satisfy the challenge so end the request cycle.
     }
-
-    return null;
-  }
-
-  private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
-    return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
-        ? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
+    // Add authorization credentials, bypassing the already-connected check.
+    successorRequestHeaders.set(requestField, credential.getHeaderValue());
+    return true;
   }
 
   /**
@@ -151,25 +155,4 @@
     }
     return result;
   }
-
-  /** An RFC 2617 challenge. */
-  private static final class Challenge {
-    final String scheme;
-    final String realm;
-
-    Challenge(String scheme, String realm) {
-      this.scheme = scheme;
-      this.realm = realm;
-    }
-
-    @Override public boolean equals(Object o) {
-      return o instanceof Challenge
-          && ((Challenge) o).scheme.equals(scheme)
-          && ((Challenge) o).realm.equals(realm);
-    }
-
-    @Override public int hashCode() {
-      return scheme.hashCode() + 31 * realm.hashCode();
-    }
-  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 8816bc4..22d4395 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -19,7 +19,6 @@
 
 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 @@
     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 @@
 
     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 @@
       sendSocketRequest();
     } else if (connection != null) {
       policy.connectionPool.recycle(connection);
+      policy.getFailedRoutes().remove(connection.getRoute());
       connection = null;
     }
   }
@@ -278,17 +278,18 @@
         hostnameVerifier = policy.hostnameVerifier;
       }
       Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
-          hostnameVerifier, policy.requestedProxy);
-      routeSelector =
-          new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT);
+          hostnameVerifier, policy.authenticator, policy.requestedProxy, policy.getTransports());
+      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 @@
   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() {
@@ -597,6 +598,7 @@
    */
   public final void readResponse() throws IOException {
     if (hasResponse()) {
+      responseHeaders.setResponseSource(responseSource);
       return;
     }
 
@@ -627,17 +629,15 @@
 
     responseHeaders = transport.readResponseHeaders();
     responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
+    responseHeaders.setResponseSource(responseSource);
 
     if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
       if (cachedResponseHeaders.validate(responseHeaders)) {
         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/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index dd7a38d..f04b317 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/src/main/java/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;
@@ -31,14 +31,6 @@
 
 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
    * the time it takes to establish a new connection.
@@ -129,22 +121,19 @@
    */
   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);
   }
 
   @Override public ResponseHeaders readResponseHeaders() throws IOException {
-    RawHeaders headers = RawHeaders.fromBytes(socketIn);
-    httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
-    httpEngine.receiveHeaders(headers);
-    return new ResponseHeaders(httpEngine.uri, headers);
+    RawHeaders rawHeaders = RawHeaders.fromBytes(socketIn);
+    httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion());
+    httpEngine.receiveHeaders(rawHeaders);
+
+    ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
+    headers.setTransport("http/1.1");
+    return headers;
   }
 
   public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
@@ -154,7 +143,7 @@
     }
 
     // We cannot reuse sockets that have incomplete output.
-    if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+    if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
       return false;
     }
 
@@ -224,7 +213,7 @@
   }
 
   /** 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 +255,7 @@
    * 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/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index ff0a2c4..c7d75a9 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -19,7 +19,11 @@
 
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.OkAuthenticator;
 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 +36,14 @@
 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.ArrayList;
 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;
@@ -60,12 +65,23 @@
  * is currently connected to a server.
  */
 public class HttpURLConnectionImpl extends HttpURLConnection {
+
+  /** Numeric status code, 307: Temporary Redirect. */
+  static final int HTTP_TEMP_REDIRECT = 307;
+
   /**
    * How many redirects should we follow? Chrome follows 21; Firefox, curl,
    * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
    */
   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. */
@@ -73,29 +89,45 @@
 
   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;
+  private List<String> transports;
+  OkAuthenticator authenticator;
+  final Set<Route> 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<Route> 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.transports = client.getTransports();
+    this.authenticator = client.getAuthenticator();
+    this.responseCache = responseCache;
+  }
+
+  Set<Route> getFailedRoutes() {
+    return failedRoutes;
+  }
+
+  List<String> getTransports() {
+    return transports;
   }
 
   @Override public final void connect() throws IOException {
@@ -212,14 +244,29 @@
   @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 {
@@ -349,29 +396,50 @@
       }
       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.
@@ -381,7 +449,7 @@
     return !sslFailure && !protocolFailure;
   }
 
-  HttpEngine getHttpEngine() {
+  public HttpEngine getHttpEngine() {
     return httpEngine;
   }
 
@@ -398,29 +466,37 @@
    */
   private Retry processResponseHeaders() throws IOException {
     Proxy selectedProxy = httpEngine.connection != null
-        ? httpEngine.connection.getProxy()
+        ? httpEngine.connection.getRoute().getProxy()
         : requestedProxy;
-    switch (getResponseCode()) {
+    final int responseCode = getResponseCode();
+    switch (responseCode) {
       case HTTP_PROXY_AUTH:
         if (selectedProxy.type() != Proxy.Type.HTTP) {
           throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
         }
         // fall-through
       case HTTP_UNAUTHORIZED:
-        boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
-            httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
+        boolean credentialsFound = HttpAuthenticator.processAuthHeader(authenticator,
+            getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
+            selectedProxy, url);
         return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
 
       case HTTP_MULT_CHOICE:
       case HTTP_MOVED_PERM:
       case HTTP_MOVED_TEMP:
       case HTTP_SEE_OTHER:
+      case HTTP_TEMP_REDIRECT:
         if (!getInstanceFollowRedirects()) {
           return Retry.NONE;
         }
         if (++redirectionCount > MAX_REDIRECTS) {
           throw new ProtocolException("Too many redirects: " + redirectionCount);
         }
+        if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
+          // "If the 307 status code is received in response to a request other than GET or HEAD,
+          // the user agent MUST NOT automatically redirect the request"
+          return Retry.NONE;
+        }
         String location = getHeaderField("Location");
         if (location == null) {
           return Retry.NONE;
@@ -476,7 +552,11 @@
     if (field == null) {
       throw new NullPointerException("field == null");
     }
-    rawRequestHeaders.set(field, newValue);
+    if ("X-Android-Transports".equals(field)) {
+      setTransports(newValue, false /* append */);
+    } else {
+      rawRequestHeaders.set(field, newValue);
+    }
   }
 
   @Override public final void addRequestProperty(String field, String value) {
@@ -486,6 +566,54 @@
     if (field == null) {
       throw new NullPointerException("field == null");
     }
-    rawRequestHeaders.add(field, value);
+
+    if ("X-Android-Transports".equals(field)) {
+      setTransports(value, true /* append */);
+    } else {
+      rawRequestHeaders.add(field, value);
+    }
+  }
+
+  /*
+   * Splits and validates a comma-separated string of transports.
+   * When append == false, we require that the transport list contains "http/1.1".
+   */
+  private void setTransports(String transportsString, boolean append) {
+    if (transportsString == null) {
+      throw new NullPointerException("transportsString == null");
+    }
+
+    String[] transports = transportsString.split(",", -1);
+    ArrayList<String> transportsList = new ArrayList<String>();
+    if (!append) {
+      // If we're not appending to the list, we need to make sure
+      // the list contains "http/1.1". We do this in a separate loop
+      // to avoid modifying any state before we validate the input.
+      boolean containsHttp = false;
+      for (int i = 0; i < transports.length; ++i) {
+        if ("http/1.1".equals(transports[i])) {
+          containsHttp = true;
+          break;
+        }
+      }
+
+      if (!containsHttp) {
+        throw new IllegalArgumentException("Transport list doesn't contain http/1.1");
+      }
+    } else {
+      transportsList.addAll(this.transports);
+    }
+
+    for (int i = 0; i < transports.length; ++i) {
+      if (transports[i].length() == 0) {
+        throw new IllegalArgumentException("Transport list contains an empty transport");
+      }
+
+      if (!transportsList.contains(transports[i])) {
+        transportsList.add(transports[i]);
+      }
+    }
+
+    this.transports = Util.immutableList(transportsList);
   }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
index c224270..235f862 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -18,6 +18,7 @@
 
 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.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 @@
   /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
   private final HttpUrlConnectionDelegate delegate;
 
-  public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
+  public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+      Set<Route> failedRoutes) {
     super(url);
-    delegate = new HttpUrlConnectionDelegate(url, client);
+    delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
   }
 
   @Override public String getCipherSuite() {
@@ -112,7 +115,7 @@
     return null;
   }
 
-  HttpEngine getHttpEngine() {
+  public HttpEngine getHttpEngine() {
     return delegate.getHttpEngine();
   }
 
@@ -399,8 +402,9 @@
   }
 
   private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
-    private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
-      super(url, client);
+    private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
+        Set<Route> failedRoutes) {
+      super(url, client, responseCache, failedRoutes);
     }
 
     @Override protected HttpURLConnection getHttpConnectionToCache() {
@@ -425,8 +429,7 @@
      * @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/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java
new file mode 100644
index 0000000..5829f02
--- /dev/null
+++ b/src/main/java/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.
+ *
+ * <p>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<String, List<String>> 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/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
new file mode 100644
index 0000000..2ac915a
--- /dev/null
+++ b/src/main/java/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<String, List<String>> 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/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
index ba83796..eba887e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
+++ b/src/main/java/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 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 (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);
     }
-    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;
-    }
+    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 @@
       String fieldName = entry.getKey();
       List<String> 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,13 +374,6 @@
       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
-          || 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/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
index 2544cee..5ec4fcc 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
@@ -22,7 +22,7 @@
 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/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
index 4e520db..97925c2 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
@@ -31,7 +31,7 @@
 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";
@@ -39,6 +39,12 @@
   /** HTTP header name for the local time when the response was received. */
   private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
 
+  /** HTTP synthetic header with the response source. */
+  static final String RESPONSE_SOURCE = "X-Android-Response-Source";
+
+  /** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc.) */
+  static final String SELECTED_TRANSPORT = "X-Android-Selected-Transport";
+
   private final URI uri;
   private final RawHeaders headers;
 
@@ -271,6 +277,14 @@
     headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
   }
 
+  public void setResponseSource(ResponseSource responseSource) {
+    headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
+  }
+
+  public void setTransport(String transport) {
+    headers.set(SELECTED_TRANSPORT, transport);
+  }
+
   /**
    * Returns the current age of the response, in milliseconds. The calculation
    * is specified by RFC 2616, 13.2.3 Age Calculations.
@@ -403,7 +417,8 @@
       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/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
index 325327d..5eb6b76 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
+++ b/src/main/java/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 @@
  * 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/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index 798cff3..ce0a71d 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -18,6 +18,7 @@
 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.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 @@
   private final ProxySelector proxySelector;
   private final ConnectionPool pool;
   private final Dns dns;
+  private final Set<Route> failedRoutes;
 
   /* The most recently attempted route. */
   private Proxy lastProxy;
@@ -64,19 +69,23 @@
   /* 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<Route> postponedRoutes;
+
   public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
-      Dns dns) {
+      Dns dns, Set<Route> failedRoutes) {
     this.address = address;
     this.uri = uri;
     this.proxySelector = proxySelector;
     this.pool = pool;
     this.dns = dns;
+    this.failedRoutes = failedRoutes;
+    this.postponedRoutes = new LinkedList<Route>();
 
     resetNextProxy(uri, address.getProxy());
   }
@@ -86,7 +95,7 @@
    * least one route.
    */
   public boolean hasNext() {
-    return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy();
+    return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
   }
 
   /**
@@ -105,7 +114,10 @@
     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 @@
       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 @@
    * 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 @@
   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 @@
       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/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index 18ab566..73709b5 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -71,7 +71,10 @@
     RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
     rawHeaders.computeResponseStatusLineFromSpdyHeaders();
     httpEngine.receiveHeaders(rawHeaders);
-    return new ResponseHeaders(httpEngine.uri, rawHeaders);
+
+    ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
+    headers.setTransport("spdy/3");
+    return headers;
   }
 
   @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index b3e248c..299da8e 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -32,8 +32,6 @@
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
-import static java.util.concurrent.Executors.defaultThreadFactory;
-
 /**
  * A socket connection to a remote peer. A connection hosts streams which can
  * send and receive data.
@@ -77,9 +75,9 @@
   static final int GOAWAY_PROTOCOL_ERROR = 1;
   static final int GOAWAY_INTERNAL_ERROR = 2;
 
-  private static final ExecutorService executor =
-      new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
-          new SynchronousQueue<Runnable>(), defaultThreadFactory());
+  private static final ExecutorService executor = new ThreadPoolExecutor(0,
+      Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
+      Util.daemonThreadFactory("OkHttp SpdyConnection"));
 
   /** True if this peer initiated the connection. */
   final boolean client;
@@ -139,17 +137,17 @@
     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;
   }
 
@@ -202,15 +200,14 @@
   }
 
   void writeSynResetLater(final int streamId, final int statusCode) {
-    executor.submit(
-        new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
-          @Override public void execute() {
-            try {
-              writeSynReset(streamId, statusCode);
-            } catch (IOException ignored) {
-            }
-          }
-        });
+    executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
+      @Override public void execute() {
+        try {
+          writeSynReset(streamId, statusCode);
+        } catch (IOException ignored) {
+        }
+      }
+    });
   }
 
   void writeSynReset(int streamId, int statusCode) throws IOException {
@@ -218,15 +215,14 @@
   }
 
   void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) {
-    executor.submit(
-        new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
-          @Override public void execute() {
-            try {
-              writeWindowUpdate(streamId, deltaWindowSize);
-            } catch (IOException ignored) {
-            }
-          }
-        });
+    executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
+      @Override public void execute() {
+        try {
+          writeWindowUpdate(streamId, deltaWindowSize);
+        } catch (IOException ignored) {
+        }
+      }
+    });
   }
 
   void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
@@ -254,7 +250,7 @@
   }
 
   private void writePingLater(final int streamId, final Ping ping) {
-    executor.submit(new NamedRunnable(String.format("Spdy Writer %s ping %d", hostName, streamId)) {
+    executor.submit(new NamedRunnable("OkHttp SPDY Writer %s ping %d", hostName, streamId) {
       @Override public void execute() {
         try {
           writePing(streamId, ping);
@@ -471,8 +467,7 @@
         return;
       }
 
-      executor.submit(
-          new NamedRunnable(String.format("Callback %s stream %d", hostName, streamId)) {
+      executor.submit(new NamedRunnable("OkHttp SPDY Callback %s stream %d", hostName, streamId) {
         @Override public void execute() {
           try {
             handler.receive(synStream);
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
index db3b50c..7d3f2bd 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -21,6 +21,7 @@
 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 @@
 
 /** 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;
@@ -232,6 +240,10 @@
     this.compressedLimit += length;
     try {
       int numberOfPairs = nameValueBlockIn.readInt();
+      if (numberOfPairs < 0) {
+        Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs);
+        throw ioException("numberOfPairs < 0");
+      }
       List<String> entries = new ArrayList<String>(numberOfPairs * 2);
       for (int i = 0; i < numberOfPairs; i++) {
         String name = readString();
@@ -248,7 +260,7 @@
 
       return entries;
     } catch (DataFormatException e) {
-      throw new IOException(e);
+      throw new IOException(e.getMessage());
     }
   }
 
diff --git a/src/main/java/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java b/src/main/java/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java
new file mode 100644
index 0000000..e0aef14
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java
@@ -0,0 +1,407 @@
+/*
+ *  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.internal.tls;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A distinguished name (DN) parser. This parser only supports extracting a
+ * string value from a DN. It doesn't support values in the hex-string style.
+ */
+final class DistinguishedNameParser {
+  private final String dn;
+  private final int length;
+  private int pos;
+  private int beg;
+  private int end;
+
+  /** Temporary variable to store positions of the currently parsed item. */
+  private int cur;
+
+  /** Distinguished name characters. */
+  private char[] chars;
+
+  public DistinguishedNameParser(X500Principal principal) {
+    // RFC2253 is used to ensure we get attributes in the reverse
+    // order of the underlying ASN.1 encoding, so that the most
+    // significant values of repeated attributes occur first.
+    this.dn = principal.getName(X500Principal.RFC2253);
+    this.length = this.dn.length();
+  }
+
+  // gets next attribute type: (ALPHA 1*keychar) / oid
+  private String nextAT() {
+    // skip preceding space chars, they can present after
+    // comma or semicolon (compatibility with RFC 1779)
+    for (; pos < length && chars[pos] == ' '; pos++) {
+    }
+    if (pos == length) {
+      return null; // reached the end of DN
+    }
+
+    // mark the beginning of attribute type
+    beg = pos;
+
+    // attribute type chars
+    pos++;
+    for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
+      // we don't follow exact BNF syntax here:
+      // accept any char except space and '='
+    }
+    if (pos >= length) {
+      throw new IllegalStateException("Unexpected end of DN: " + dn);
+    }
+
+    // mark the end of attribute type
+    end = pos;
+
+    // skip trailing space chars between attribute type and '='
+    // (compatibility with RFC 1779)
+    if (chars[pos] == ' ') {
+      for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
+      }
+
+      if (chars[pos] != '=' || pos == length) {
+        throw new IllegalStateException("Unexpected end of DN: " + dn);
+      }
+    }
+
+    pos++; //skip '=' char
+
+    // skip space chars between '=' and attribute value
+    // (compatibility with RFC 1779)
+    for (; pos < length && chars[pos] == ' '; pos++) {
+    }
+
+    // in case of oid attribute type skip its prefix: "oid." or "OID."
+    // (compatibility with RFC 1779)
+    if ((end - beg > 4) && (chars[beg + 3] == '.')
+        && (chars[beg] == 'O' || chars[beg] == 'o')
+        && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
+        && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
+      beg += 4;
+    }
+
+    return new String(chars, beg, end - beg);
+  }
+
+  // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
+  private String quotedAV() {
+    pos++;
+    beg = pos;
+    end = beg;
+    while (true) {
+
+      if (pos == length) {
+        throw new IllegalStateException("Unexpected end of DN: " + dn);
+      }
+
+      if (chars[pos] == '"') {
+        // enclosing quotation was found
+        pos++;
+        break;
+      } else if (chars[pos] == '\\') {
+        chars[end] = getEscaped();
+      } else {
+        // shift char: required for string with escaped chars
+        chars[end] = chars[pos];
+      }
+      pos++;
+      end++;
+    }
+
+    // skip trailing space chars before comma or semicolon.
+    // (compatibility with RFC 1779)
+    for (; pos < length && chars[pos] == ' '; pos++) {
+    }
+
+    return new String(chars, beg, end - beg);
+  }
+
+  // gets hex string attribute value: "#" hexstring
+  private String hexAV() {
+    if (pos + 4 >= length) {
+      // encoded byte array  must be not less then 4 c
+      throw new IllegalStateException("Unexpected end of DN: " + dn);
+    }
+
+    beg = pos; // store '#' position
+    pos++;
+    while (true) {
+
+      // check for end of attribute value
+      // looks for space and component separators
+      if (pos == length || chars[pos] == '+' || chars[pos] == ','
+          || chars[pos] == ';') {
+        end = pos;
+        break;
+      }
+
+      if (chars[pos] == ' ') {
+        end = pos;
+        pos++;
+        // skip trailing space chars before comma or semicolon.
+        // (compatibility with RFC 1779)
+        for (; pos < length && chars[pos] == ' '; pos++) {
+        }
+        break;
+      } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
+        chars[pos] += 32; //to low case
+      }
+
+      pos++;
+    }
+
+    // verify length of hex string
+    // encoded byte array  must be not less then 4 and must be even number
+    int hexLen = end - beg; // skip first '#' char
+    if (hexLen < 5 || (hexLen & 1) == 0) {
+      throw new IllegalStateException("Unexpected end of DN: " + dn);
+    }
+
+    // get byte encoding from string representation
+    byte[] encoded = new byte[hexLen / 2];
+    for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
+      encoded[i] = (byte) getByte(p);
+    }
+
+    return new String(chars, beg, hexLen);
+  }
+
+  // gets string attribute value: *( stringchar / pair )
+  private String escapedAV() {
+    beg = pos;
+    end = pos;
+    while (true) {
+      if (pos >= length) {
+        // the end of DN has been found
+        return new String(chars, beg, end - beg);
+      }
+
+      switch (chars[pos]) {
+        case '+':
+        case ',':
+        case ';':
+          // separator char has been found
+          return new String(chars, beg, end - beg);
+        case '\\':
+          // escaped char
+          chars[end++] = getEscaped();
+          pos++;
+          break;
+        case ' ':
+          // need to figure out whether space defines
+          // the end of attribute value or not
+          cur = end;
+
+          pos++;
+          chars[end++] = ' ';
+
+          for (; pos < length && chars[pos] == ' '; pos++) {
+            chars[end++] = ' ';
+          }
+          if (pos == length || chars[pos] == ',' || chars[pos] == '+'
+              || chars[pos] == ';') {
+            // separator char or the end of DN has been found
+            return new String(chars, beg, cur - beg);
+          }
+          break;
+        default:
+          chars[end++] = chars[pos];
+          pos++;
+      }
+    }
+  }
+
+  // returns escaped char
+  private char getEscaped() {
+    pos++;
+    if (pos == length) {
+      throw new IllegalStateException("Unexpected end of DN: " + dn);
+    }
+
+    switch (chars[pos]) {
+      case '"':
+      case '\\':
+      case ',':
+      case '=':
+      case '+':
+      case '<':
+      case '>':
+      case '#':
+      case ';':
+      case ' ':
+      case '*':
+      case '%':
+      case '_':
+        //FIXME: escaping is allowed only for leading or trailing space char
+        return chars[pos];
+      default:
+        // RFC doesn't explicitly say that escaped hex pair is
+        // interpreted as UTF-8 char. It only contains an example of such DN.
+        return getUTF8();
+    }
+  }
+
+  // decodes UTF-8 char
+  // see http://www.unicode.org for UTF-8 bit distribution table
+  private char getUTF8() {
+    int res = getByte(pos);
+    pos++; //FIXME tmp
+
+    if (res < 128) { // one byte: 0-7F
+      return (char) res;
+    } else if (res >= 192 && res <= 247) {
+
+      int count;
+      if (res <= 223) { // two bytes: C0-DF
+        count = 1;
+        res = res & 0x1F;
+      } else if (res <= 239) { // three bytes: E0-EF
+        count = 2;
+        res = res & 0x0F;
+      } else { // four bytes: F0-F7
+        count = 3;
+        res = res & 0x07;
+      }
+
+      int b;
+      for (int i = 0; i < count; i++) {
+        pos++;
+        if (pos == length || chars[pos] != '\\') {
+          return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+        }
+        pos++;
+
+        b = getByte(pos);
+        pos++; //FIXME tmp
+        if ((b & 0xC0) != 0x80) {
+          return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+        }
+
+        res = (res << 6) + (b & 0x3F);
+      }
+      return (char) res;
+    } else {
+      return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+    }
+  }
+
+  // Returns byte representation of a char pair
+  // The char pair is composed of DN char in
+  // specified 'position' and the next char
+  // According to BNF syntax:
+  // hexchar    = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+  //                    / "a" / "b" / "c" / "d" / "e" / "f"
+  private int getByte(int position) {
+    if (position + 1 >= length) {
+      throw new IllegalStateException("Malformed DN: " + dn);
+    }
+
+    int b1, b2;
+
+    b1 = chars[position];
+    if (b1 >= '0' && b1 <= '9') {
+      b1 = b1 - '0';
+    } else if (b1 >= 'a' && b1 <= 'f') {
+      b1 = b1 - 87; // 87 = 'a' - 10
+    } else if (b1 >= 'A' && b1 <= 'F') {
+      b1 = b1 - 55; // 55 = 'A' - 10
+    } else {
+      throw new IllegalStateException("Malformed DN: " + dn);
+    }
+
+    b2 = chars[position + 1];
+    if (b2 >= '0' && b2 <= '9') {
+      b2 = b2 - '0';
+    } else if (b2 >= 'a' && b2 <= 'f') {
+      b2 = b2 - 87; // 87 = 'a' - 10
+    } else if (b2 >= 'A' && b2 <= 'F') {
+      b2 = b2 - 55; // 55 = 'A' - 10
+    } else {
+      throw new IllegalStateException("Malformed DN: " + dn);
+    }
+
+    return (b1 << 4) + b2;
+  }
+
+  /**
+   * Parses the DN and returns the most significant attribute value
+   * for an attribute type, or null if none found.
+   *
+   * @param attributeType attribute type to look for (e.g. "ca")
+   */
+  public String findMostSpecific(String attributeType) {
+    // Initialize internal state.
+    pos = 0;
+    beg = 0;
+    end = 0;
+    cur = 0;
+    chars = dn.toCharArray();
+
+    String attType = nextAT();
+    if (attType == null) {
+      return null;
+    }
+    while (true) {
+      String attValue = "";
+
+      if (pos == length) {
+        return null;
+      }
+
+      switch (chars[pos]) {
+        case '"':
+          attValue = quotedAV();
+          break;
+        case '#':
+          attValue = hexAV();
+          break;
+        case '+':
+        case ',':
+        case ';': // compatibility with RFC 1779: semicolon can separate RDNs
+          //empty attribute value
+          break;
+        default:
+          attValue = escapedAV();
+      }
+
+      // Values are ordered from most specific to least specific
+      // due to the RFC2253 formatting. So take the first match
+      // we see.
+      if (attributeType.equalsIgnoreCase(attType)) {
+        return attValue;
+      }
+
+      if (pos >= length) {
+        return null;
+      }
+
+      if (chars[pos] == ',' || chars[pos] == ';') {
+      } else if (chars[pos] != '+') {
+        throw new IllegalStateException("Malformed DN: " + dn);
+      }
+
+      pos++;
+      attType = nextAT();
+      if (attType == null) {
+        throw new IllegalStateException("Malformed DN: " + dn);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java b/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
new file mode 100644
index 0000000..fbe8c68
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
@@ -0,0 +1,189 @@
+/*
+ *  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.internal.tls;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A HostnameVerifier consistent with <a
+ * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
+ */
+public final class OkHostnameVerifier implements HostnameVerifier {
+  /**
+   * Quick and dirty pattern to differentiate IP addresses from hostnames. This
+   * is an approximation of Android's private InetAddress#isNumeric API.
+   *
+   * <p>This matches IPv6 addresses as a hex string containing at least one
+   * colon, and possibly including dots after the first colon. It matches IPv4
+   * addresses as strings containing only decimal digits and dots. This pattern
+   * matches strings like "a:.23" and "54" that are neither IP addresses nor
+   * hostnames; they will be verified as IP addresses (which is a more strict
+   * verification).
+   */
+  private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
+      "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
+
+  private static final int ALT_DNS_NAME = 2;
+  private static final int ALT_IPA_NAME = 7;
+
+  public boolean verify(String host, SSLSession session) {
+    try {
+      Certificate[] certificates = session.getPeerCertificates();
+      return verify(host, (X509Certificate) certificates[0]);
+    } catch (SSLException e) {
+      return false;
+    }
+  }
+
+  public boolean verify(String host, X509Certificate certificate) {
+    return verifyAsIpAddress(host)
+        ? verifyIpAddress(host, certificate)
+        : verifyHostName(host, certificate);
+  }
+
+  static boolean verifyAsIpAddress(String host) {
+    return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
+  }
+
+  /**
+   * Returns true if {@code certificate} matches {@code ipAddress}.
+   */
+  private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
+    for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) {
+      if (ipAddress.equalsIgnoreCase(altName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns true if {@code certificate} matches {@code hostName}.
+   */
+  private boolean verifyHostName(String hostName, X509Certificate certificate) {
+    hostName = hostName.toLowerCase(Locale.US);
+    boolean hasDns = false;
+    for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) {
+      hasDns = true;
+      if (verifyHostName(hostName, altName)) {
+        return true;
+      }
+    }
+
+    if (!hasDns) {
+      X500Principal principal = certificate.getSubjectX500Principal();
+      // RFC 2818 advises using the most specific name for matching.
+      String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
+      if (cn != null) {
+        return verifyHostName(hostName, cn);
+      }
+    }
+
+    return false;
+  }
+
+  private List<String> getSubjectAltNames(X509Certificate certificate, int type) {
+    List<String> result = new ArrayList<String>();
+    try {
+      Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
+      if (subjectAltNames == null) {
+        return Collections.emptyList();
+      }
+      for (Object subjectAltName : subjectAltNames) {
+        List<?> entry = (List<?>) subjectAltName;
+        if (entry == null || entry.size() < 2) {
+          continue;
+        }
+        Integer altNameType = (Integer) entry.get(0);
+        if (altNameType == null) {
+          continue;
+        }
+        if (altNameType == type) {
+          String altName = (String) entry.get(1);
+          if (altName != null) {
+            result.add(altName);
+          }
+        }
+      }
+      return result;
+    } catch (CertificateParsingException e) {
+      return Collections.emptyList();
+    }
+  }
+
+  /**
+   * Returns true if {@code hostName} matches the name or pattern {@code cn}.
+   *
+   * @param hostName lowercase host name.
+   * @param cn certificate host name. May include wildcards like
+   *     {@code *.android.com}.
+   */
+  public boolean verifyHostName(String hostName, String cn) {
+    // Check length == 0 instead of .isEmpty() to support Java 5.
+    if (hostName == null || hostName.length() == 0 || cn == null || cn.length() == 0) {
+      return false;
+    }
+
+    cn = cn.toLowerCase(Locale.US);
+
+    if (!cn.contains("*")) {
+      return hostName.equals(cn);
+    }
+
+    if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
+      return true; // "*.foo.com" matches "foo.com"
+    }
+
+    int asterisk = cn.indexOf('*');
+    int dot = cn.indexOf('.');
+    if (asterisk > dot) {
+      return false; // malformed; wildcard must be in the first part of the cn
+    }
+
+    if (!hostName.regionMatches(0, cn, 0, asterisk)) {
+      return false; // prefix before '*' doesn't match
+    }
+
+    int suffixLength = cn.length() - (asterisk + 1);
+    int suffixStart = hostName.length() - suffixLength;
+    if (hostName.indexOf('.', asterisk) < suffixStart) {
+      // TODO: remove workaround for *.clients.google.com http://b/5426333
+      if (!hostName.endsWith(".clients.google.com")) {
+        return false; // wildcard '*' can't match a '.'
+      }
+    }
+
+    if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) {
+      return false; // suffix after '*' doesn't match
+    }
+
+    return true;
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index dca9625..a3a9ea8 100644
--- a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -19,6 +19,7 @@
 import com.squareup.okhttp.internal.RecordingHostnameVerifier;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpAuthenticator;
 import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
 import java.io.IOException;
 import java.net.InetAddress;
@@ -70,30 +71,33 @@
 
   @Before public void setUp() throws Exception {
     httpServer.play();
-    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null, null);
+    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null,
+        HttpAuthenticator.SYSTEM_DEFAULT, null, Arrays.asList("spdy/3", "http/1.1"));
     httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
         httpServer.getPort());
 
     spdyServer.play();
-    spdyAddress =
-        new Address(spdyServer.getHostName(), spdyServer.getPort(), sslContext.getSocketFactory(),
-            new RecordingHostnameVerifier(), null);
+    spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(),
+        sslContext.getSocketFactory(), new RecordingHostnameVerifier(),
+        HttpAuthenticator.SYSTEM_DEFAULT, null, Arrays.asList("spdy/3", "http/1.1"));
     spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
         spdyServer.getPort());
 
-    httpA = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+    httpA = new Connection(httpRoute);
     httpA.connect(100, 100, null);
-    httpB = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpB = new Connection(httpRoute);
     httpB.connect(100, 100, null);
-    httpC = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpC = new Connection(httpRoute);
     httpC.connect(100, 100, null);
-    httpD = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpD = new Connection(httpRoute);
     httpD.connect(100, 100, null);
-    httpE = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpE = new Connection(httpRoute);
     httpE.connect(100, 100, null);
-    spdyA = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+    spdyA = new Connection(spdyRoute);
     spdyA.connect(100, 100, null);
-    spdyB = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+    spdyB = new Connection(spdyRoute);
     spdyB.connect(100, 100, null);
   }
 
@@ -115,7 +119,7 @@
     Connection connection = pool.get(httpAddress);
     assertNull(connection);
 
-    connection = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    connection = new Connection(new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true));
     connection.connect(100, 100, null);
     assertEquals(0, pool.getConnectionCount());
     pool.recycle(connection);
@@ -385,6 +389,18 @@
     assertEquals(0, pool.getSpdyConnectionCount());
   }
 
+  @Test public void evictAllConnections() {
+    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+    pool.recycle(httpA);
+    Util.closeQuietly(httpA); // Include a closed connection in the pool.
+    pool.recycle(httpB);
+    pool.maybeShare(spdyA);
+    assertEquals(3, pool.getConnectionCount());
+
+    pool.evictAll();
+    assertEquals(0, pool.getConnectionCount());
+  }
+
   private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
     assertEquals(Arrays.asList(connections), pool.getConnections());
   }
diff --git a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java b/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
deleted file mode 100644
index 72cc70d..0000000
--- a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
+++ /dev/null
@@ -1,802 +0,0 @@
-/*
- * Copyright (C) 2011 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.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.InputStream;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
-import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
-import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public final class DiskLruCacheTest {
-  private final int appVersion = 100;
-  private String javaTmpDir;
-  private File cacheDir;
-  private File journalFile;
-  private DiskLruCache cache;
-
-  @Before public void setUp() throws Exception {
-    javaTmpDir = System.getProperty("java.io.tmpdir");
-    cacheDir = new File(javaTmpDir, "DiskLruCacheTest");
-    cacheDir.mkdir();
-    journalFile = new File(cacheDir, JOURNAL_FILE);
-    for (File file : cacheDir.listFiles()) {
-      file.delete();
-    }
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-  }
-
-  @After public void tearDown() throws Exception {
-    cache.close();
-  }
-
-  @Test public void emptyCache() throws Exception {
-    cache.close();
-    assertJournalEquals();
-  }
-
-  @Test public void writeAndReadEntry() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    creator.set(0, "ABC");
-    creator.set(1, "DE");
-    assertNull(creator.getString(0));
-    assertNull(creator.newInputStream(0));
-    assertNull(creator.getString(1));
-    assertNull(creator.newInputStream(1));
-    creator.commit();
-
-    DiskLruCache.Snapshot snapshot = cache.get("k1");
-    assertEquals("ABC", snapshot.getString(0));
-    assertEquals("DE", snapshot.getString(1));
-  }
-
-  @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    creator.set(0, "A");
-    creator.set(1, "B");
-    creator.commit();
-    cache.close();
-
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    DiskLruCache.Snapshot snapshot = cache.get("k1");
-    assertEquals("A", snapshot.getString(0));
-    assertEquals("B", snapshot.getString(1));
-    snapshot.close();
-  }
-
-  @Test public void journalWithEditAndPublish() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
-    creator.set(0, "AB");
-    creator.set(1, "C");
-    creator.commit();
-    cache.close();
-    assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
-  }
-
-  @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
-    creator.set(0, "AB");
-    creator.set(1, "C");
-    creator.abort();
-    cache.close();
-    assertJournalEquals("DIRTY k1", "REMOVE k1");
-  }
-
-  @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
-    cache.edit("k1");
-    cache.close();
-    assertJournalEquals("DIRTY k1", "REMOVE k1");
-  }
-
-  @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    assertNull(cache.get("k1"));
-    creator.set(0, "A");
-    creator.set(1, "BC");
-    creator.commit();
-    cache.close();
-    assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
-  }
-
-  @Test public void journalWithEditAndPublishAndRead() throws Exception {
-    DiskLruCache.Editor k1Creator = cache.edit("k1");
-    k1Creator.set(0, "AB");
-    k1Creator.set(1, "C");
-    k1Creator.commit();
-    DiskLruCache.Editor k2Creator = cache.edit("k2");
-    k2Creator.set(0, "DEF");
-    k2Creator.set(1, "G");
-    k2Creator.commit();
-    DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
-    k1Snapshot.close();
-    cache.close();
-    assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
-  }
-
-  @Test public void cannotOperateOnEditAfterPublish() throws Exception {
-    DiskLruCache.Editor editor = cache.edit("k1");
-    editor.set(0, "A");
-    editor.set(1, "B");
-    editor.commit();
-    assertInoperable(editor);
-  }
-
-  @Test public void cannotOperateOnEditAfterRevert() throws Exception {
-    DiskLruCache.Editor editor = cache.edit("k1");
-    editor.set(0, "A");
-    editor.set(1, "B");
-    editor.abort();
-    assertInoperable(editor);
-  }
-
-  @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
-    DiskLruCache.Editor editor = cache.edit("k1");
-    editor.set(0, "ABC");
-    editor.set(1, "B");
-    editor.commit();
-    File k1 = getCleanFile("k1", 0);
-    assertEquals("ABC", readFile(k1));
-    cache.remove("k1");
-    assertFalse(k1.exists());
-  }
-
-  /**
-   * Each read sees a snapshot of the file at the time read was called.
-   * This means that two reads of the same key can see different data.
-   */
-  @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
-    DiskLruCache.Editor v1Creator = cache.edit("k1");
-    v1Creator.set(0, "AAaa");
-    v1Creator.set(1, "BBbb");
-    v1Creator.commit();
-
-    DiskLruCache.Snapshot snapshot1 = cache.get("k1");
-    InputStream inV1 = snapshot1.getInputStream(0);
-    assertEquals('A', inV1.read());
-    assertEquals('A', inV1.read());
-
-    DiskLruCache.Editor v1Updater = cache.edit("k1");
-    v1Updater.set(0, "CCcc");
-    v1Updater.set(1, "DDdd");
-    v1Updater.commit();
-
-    DiskLruCache.Snapshot snapshot2 = cache.get("k1");
-    assertEquals("CCcc", snapshot2.getString(0));
-    assertEquals("DDdd", snapshot2.getString(1));
-    snapshot2.close();
-
-    assertEquals('a', inV1.read());
-    assertEquals('a', inV1.read());
-    assertEquals("BBbb", snapshot1.getString(1));
-    snapshot1.close();
-  }
-
-  @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
-    cache.close();
-    File cleanFile0 = getCleanFile("k1", 0);
-    File cleanFile1 = getCleanFile("k1", 1);
-    File dirtyFile0 = getDirtyFile("k1", 0);
-    File dirtyFile1 = getDirtyFile("k1", 1);
-    writeFile(cleanFile0, "A");
-    writeFile(cleanFile1, "B");
-    writeFile(dirtyFile0, "C");
-    writeFile(dirtyFile1, "D");
-    createJournal("CLEAN k1 1 1", "DIRTY   k1");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertFalse(cleanFile0.exists());
-    assertFalse(cleanFile1.exists());
-    assertFalse(dirtyFile0.exists());
-    assertFalse(dirtyFile1.exists());
-    assertNull(cache.get("k1"));
-  }
-
-  @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournalWithHeader(MAGIC, "0", "100", "2", "");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-  }
-
-  @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournalWithHeader(MAGIC, "1", "101", "2", "");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-  }
-
-  @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournalWithHeader(MAGIC, "1", "100", "1", "");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-  }
-
-  @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournalWithHeader(MAGIC, "1", "100", "2", "x");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-  }
-
-  @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournal("CLEAN k1 1 1", "BOGUS");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-    assertNull(cache.get("k1"));
-  }
-
-  @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournal("CLEAN k1 0000x001 1");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-    assertNull(cache.get("k1"));
-  }
-
-  @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
-    cache.close();
-    writeFile(getCleanFile("k1", 0), "A");
-    writeFile(getCleanFile("k1", 1), "B");
-    Writer writer = new FileWriter(journalFile);
-    writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
-    writer.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertNull(cache.get("k1"));
-  }
-
-  @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
-    cache.close();
-    generateSomeGarbageFiles();
-    createJournal("CLEAN k1 1 1 1");
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-    assertGarbageFilesAllDeleted();
-    assertNull(cache.get("k1"));
-  }
-
-  @Test public void keyWithSpaceNotPermitted() throws Exception {
-    try {
-      cache.edit("my key");
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
-  }
-
-  @Test public void keyWithNewlineNotPermitted() throws Exception {
-    try {
-      cache.edit("my\nkey");
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
-  }
-
-  @Test public void keyWithCarriageReturnNotPermitted() throws Exception {
-    try {
-      cache.edit("my\rkey");
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
-  }
-
-  @Test public void nullKeyThrows() throws Exception {
-    try {
-      cache.edit(null);
-      fail();
-    } catch (NullPointerException expected) {
-    }
-  }
-
-  @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    creator.set(1, "A");
-    try {
-      creator.commit();
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-
-    assertFalse(getCleanFile("k1", 0).exists());
-    assertFalse(getCleanFile("k1", 1).exists());
-    assertFalse(getDirtyFile("k1", 0).exists());
-    assertFalse(getDirtyFile("k1", 1).exists());
-    assertNull(cache.get("k1"));
-
-    DiskLruCache.Editor creator2 = cache.edit("k1");
-    creator2.set(0, "B");
-    creator2.set(1, "C");
-    creator2.commit();
-  }
-
-  @Test public void createNewEntryWithMissingFileAborts() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    creator.set(0, "A");
-    creator.set(1, "A");
-    assertTrue(getDirtyFile("k1", 0).exists());
-    assertTrue(getDirtyFile("k1", 1).exists());
-    assertTrue(getDirtyFile("k1", 0).delete());
-    assertFalse(getDirtyFile("k1", 0).exists());
-    creator.commit();  // silently abort if file does not exist due to I/O issue
-
-    assertFalse(getCleanFile("k1", 0).exists());
-    assertFalse(getCleanFile("k1", 1).exists());
-    assertFalse(getDirtyFile("k1", 0).exists());
-    assertFalse(getDirtyFile("k1", 1).exists());
-    assertNull(cache.get("k1"));
-
-    DiskLruCache.Editor creator2 = cache.edit("k1");
-    creator2.set(0, "B");
-    creator2.set(1, "C");
-    creator2.commit();
-  }
-
-  @Test public void revertWithTooFewValues() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    creator.set(1, "A");
-    creator.abort();
-    assertFalse(getCleanFile("k1", 0).exists());
-    assertFalse(getCleanFile("k1", 1).exists());
-    assertFalse(getDirtyFile("k1", 0).exists());
-    assertFalse(getDirtyFile("k1", 1).exists());
-    assertNull(cache.get("k1"));
-  }
-
-  @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
-    DiskLruCache.Editor creator = cache.edit("k1");
-    creator.set(0, "A");
-    creator.set(1, "B");
-    creator.commit();
-
-    DiskLruCache.Editor updater = cache.edit("k1");
-    updater.set(0, "C");
-    updater.commit();
-
-    DiskLruCache.Snapshot snapshot = cache.get("k1");
-    assertEquals("C", snapshot.getString(0));
-    assertEquals("B", snapshot.getString(1));
-    snapshot.close();
-  }
-
-  @Test public void evictOnInsert() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
-    set("A", "a", "aaa"); // size 4
-    set("B", "bb", "bbbb"); // size 6
-    assertEquals(10, cache.size());
-
-    // cause the size to grow to 12 should evict 'A'
-    set("C", "c", "c");
-    cache.flush();
-    assertEquals(8, cache.size());
-    assertAbsent("A");
-    assertValue("B", "bb", "bbbb");
-    assertValue("C", "c", "c");
-
-    // causing the size to grow to 10 should evict nothing
-    set("D", "d", "d");
-    cache.flush();
-    assertEquals(10, cache.size());
-    assertAbsent("A");
-    assertValue("B", "bb", "bbbb");
-    assertValue("C", "c", "c");
-    assertValue("D", "d", "d");
-
-    // causing the size to grow to 18 should evict 'B' and 'C'
-    set("E", "eeee", "eeee");
-    cache.flush();
-    assertEquals(10, cache.size());
-    assertAbsent("A");
-    assertAbsent("B");
-    assertAbsent("C");
-    assertValue("D", "d", "d");
-    assertValue("E", "eeee", "eeee");
-  }
-
-  @Test public void evictOnUpdate() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
-    set("A", "a", "aa"); // size 3
-    set("B", "b", "bb"); // size 3
-    set("C", "c", "cc"); // size 3
-    assertEquals(9, cache.size());
-
-    // causing the size to grow to 11 should evict 'A'
-    set("B", "b", "bbbb");
-    cache.flush();
-    assertEquals(8, cache.size());
-    assertAbsent("A");
-    assertValue("B", "b", "bbbb");
-    assertValue("C", "c", "cc");
-  }
-
-  @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-    set("A", "a", "a");
-    set("B", "b", "b");
-    set("C", "c", "c");
-    set("D", "d", "d");
-    set("E", "e", "e");
-    cache.get("B").close(); // 'B' is now least recently used
-
-    // causing the size to grow to 12 should evict 'A'
-    set("F", "f", "f");
-    // causing the size to grow to 12 should evict 'C'
-    set("G", "g", "g");
-    cache.flush();
-    assertEquals(10, cache.size());
-    assertAbsent("A");
-    assertValue("B", "b", "b");
-    assertAbsent("C");
-    assertValue("D", "d", "d");
-    assertValue("E", "e", "e");
-    assertValue("F", "f", "f");
-  }
-
-  @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
-    set("A", "a", "a");
-    set("B", "b", "b");
-    set("C", "c", "c");
-    set("D", "d", "d");
-    set("E", "e", "e");
-    set("F", "f", "f");
-    cache.get("B").close(); // 'B' is now least recently used
-    assertEquals(12, cache.size());
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
-    set("G", "g", "g");
-    cache.flush();
-    assertEquals(10, cache.size());
-    assertAbsent("A");
-    assertValue("B", "b", "b");
-    assertAbsent("C");
-    assertValue("D", "d", "d");
-    assertValue("E", "e", "e");
-    assertValue("F", "f", "f");
-    assertValue("G", "g", "g");
-  }
-
-  @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-    set("A", "aaaaa", "aaaaaa"); // size=11
-    cache.flush();
-    assertAbsent("A");
-  }
-
-  @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-    set("A", "aaaaaaaaaaa", "a"); // size=12
-    cache.flush();
-    assertAbsent("A");
-  }
-
-  @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
-    try {
-      DiskLruCache.open(cacheDir, appVersion, 2, 0);
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
-  }
-
-  @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
-    try {
-      DiskLruCache.open(cacheDir, appVersion, 0, 10);
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
-  }
-
-  @Test public void removeAbsentElement() throws Exception {
-    cache.remove("A");
-  }
-
-  @Test public void readingTheSameStreamMultipleTimes() throws Exception {
-    set("A", "a", "b");
-    DiskLruCache.Snapshot snapshot = cache.get("A");
-    assertSame(snapshot.getInputStream(0), snapshot.getInputStream(0));
-    snapshot.close();
-  }
-
-  @Test public void rebuildJournalOnRepeatedReads() throws Exception {
-    set("A", "a", "a");
-    set("B", "b", "b");
-    long lastJournalLength = 0;
-    while (true) {
-      long journalLength = journalFile.length();
-      assertValue("A", "a", "a");
-      assertValue("B", "b", "b");
-      if (journalLength < lastJournalLength) {
-        System.out
-            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
-                journalLength);
-        break; // test passed!
-      }
-      lastJournalLength = journalLength;
-    }
-  }
-
-  @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
-    long lastJournalLength = 0;
-    while (true) {
-      long journalLength = journalFile.length();
-      set("A", "a", "a");
-      set("B", "b", "b");
-      if (journalLength < lastJournalLength) {
-        System.out
-            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
-                journalLength);
-        break;
-      }
-      lastJournalLength = journalLength;
-    }
-
-    // sanity check that a rebuilt journal behaves normally
-    assertValue("A", "a", "a");
-    assertValue("B", "b", "b");
-  }
-
-  @Test public void openCreatesDirectoryIfNecessary() throws Exception {
-    cache.close();
-    File dir = new File(javaTmpDir, "testOpenCreatesDirectoryIfNecessary");
-    cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE);
-    set("A", "a", "a");
-    assertTrue(new File(dir, "A.0").exists());
-    assertTrue(new File(dir, "A.1").exists());
-    assertTrue(new File(dir, "journal").exists());
-  }
-
-  @Test public void fileDeletedExternally() throws Exception {
-    set("A", "a", "a");
-    getCleanFile("A", 1).delete();
-    assertNull(cache.get("A"));
-  }
-
-  @Test public void editSameVersion() throws Exception {
-    set("A", "a", "a");
-    DiskLruCache.Snapshot snapshot = cache.get("A");
-    DiskLruCache.Editor editor = snapshot.edit();
-    editor.set(1, "a2");
-    editor.commit();
-    assertValue("A", "a", "a2");
-  }
-
-  @Test public void editSnapshotAfterChangeAborted() throws Exception {
-    set("A", "a", "a");
-    DiskLruCache.Snapshot snapshot = cache.get("A");
-    DiskLruCache.Editor toAbort = snapshot.edit();
-    toAbort.set(0, "b");
-    toAbort.abort();
-    DiskLruCache.Editor editor = snapshot.edit();
-    editor.set(1, "a2");
-    editor.commit();
-    assertValue("A", "a", "a2");
-  }
-
-  @Test public void editSnapshotAfterChangeCommitted() throws Exception {
-    set("A", "a", "a");
-    DiskLruCache.Snapshot snapshot = cache.get("A");
-    DiskLruCache.Editor toAbort = snapshot.edit();
-    toAbort.set(0, "b");
-    toAbort.commit();
-    assertNull(snapshot.edit());
-  }
-
-  @Test public void editSinceEvicted() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-    set("A", "aa", "aaa"); // size 5
-    DiskLruCache.Snapshot snapshot = cache.get("A");
-    set("B", "bb", "bbb"); // size 5
-    set("C", "cc", "ccc"); // size 5; will evict 'A'
-    cache.flush();
-    assertNull(snapshot.edit());
-  }
-
-  @Test public void editSinceEvictedAndRecreated() throws Exception {
-    cache.close();
-    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-    set("A", "aa", "aaa"); // size 5
-    DiskLruCache.Snapshot snapshot = cache.get("A");
-    set("B", "bb", "bbb"); // size 5
-    set("C", "cc", "ccc"); // size 5; will evict 'A'
-    set("A", "a", "aaaa"); // size 5; will evict 'B'
-    cache.flush();
-    assertNull(snapshot.edit());
-  }
-
-  private void assertJournalEquals(String... expectedBodyLines) throws Exception {
-    List<String> expectedLines = new ArrayList<String>();
-    expectedLines.add(MAGIC);
-    expectedLines.add(VERSION_1);
-    expectedLines.add("100");
-    expectedLines.add("2");
-    expectedLines.add("");
-    expectedLines.addAll(Arrays.asList(expectedBodyLines));
-    assertEquals(expectedLines, readJournalLines());
-  }
-
-  private void createJournal(String... bodyLines) throws Exception {
-    createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
-  }
-
-  private void createJournalWithHeader(String magic, String version, String appVersion,
-      String valueCount, String blank, String... bodyLines) throws Exception {
-    Writer writer = new FileWriter(journalFile);
-    writer.write(magic + "\n");
-    writer.write(version + "\n");
-    writer.write(appVersion + "\n");
-    writer.write(valueCount + "\n");
-    writer.write(blank + "\n");
-    for (String line : bodyLines) {
-      writer.write(line);
-      writer.write('\n');
-    }
-    writer.close();
-  }
-
-  private List<String> readJournalLines() throws Exception {
-    List<String> result = new ArrayList<String>();
-    BufferedReader reader = new BufferedReader(new FileReader(journalFile));
-    String line;
-    while ((line = reader.readLine()) != null) {
-      result.add(line);
-    }
-    reader.close();
-    return result;
-  }
-
-  private File getCleanFile(String key, int index) {
-    return new File(cacheDir, key + "." + index);
-  }
-
-  private File getDirtyFile(String key, int index) {
-    return new File(cacheDir, key + "." + index + ".tmp");
-  }
-
-  private String readFile(File file) throws Exception {
-    Reader reader = new FileReader(file);
-    StringWriter writer = new StringWriter();
-    char[] buffer = new char[1024];
-    int count;
-    while ((count = reader.read(buffer)) != -1) {
-      writer.write(buffer, 0, count);
-    }
-    reader.close();
-    return writer.toString();
-  }
-
-  public void writeFile(File file, String content) throws Exception {
-    FileWriter writer = new FileWriter(file);
-    writer.write(content);
-    writer.close();
-  }
-
-  private void assertInoperable(DiskLruCache.Editor editor) throws Exception {
-    try {
-      editor.getString(0);
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-    try {
-      editor.set(0, "A");
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-    try {
-      editor.newInputStream(0);
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-    try {
-      editor.newOutputStream(0);
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-    try {
-      editor.commit();
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-    try {
-      editor.abort();
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-  }
-
-  private void generateSomeGarbageFiles() throws Exception {
-    File dir1 = new File(cacheDir, "dir1");
-    File dir2 = new File(dir1, "dir2");
-    writeFile(getCleanFile("g1", 0), "A");
-    writeFile(getCleanFile("g1", 1), "B");
-    writeFile(getCleanFile("g2", 0), "C");
-    writeFile(getCleanFile("g2", 1), "D");
-    writeFile(getCleanFile("g2", 1), "D");
-    writeFile(new File(cacheDir, "otherFile0"), "E");
-    dir1.mkdir();
-    dir2.mkdir();
-    writeFile(new File(dir2, "otherFile1"), "F");
-  }
-
-  private void assertGarbageFilesAllDeleted() throws Exception {
-    assertFalse(getCleanFile("g1", 0).exists());
-    assertFalse(getCleanFile("g1", 1).exists());
-    assertFalse(getCleanFile("g2", 0).exists());
-    assertFalse(getCleanFile("g2", 1).exists());
-    assertFalse(new File(cacheDir, "otherFile0").exists());
-    assertFalse(new File(cacheDir, "dir1").exists());
-  }
-
-  private void set(String key, String value0, String value1) throws Exception {
-    DiskLruCache.Editor editor = cache.edit(key);
-    editor.set(0, value0);
-    editor.set(1, value1);
-    editor.commit();
-  }
-
-  private void assertAbsent(String key) throws Exception {
-    DiskLruCache.Snapshot snapshot = cache.get(key);
-    if (snapshot != null) {
-      snapshot.close();
-      fail();
-    }
-    assertFalse(getCleanFile(key, 0).exists());
-    assertFalse(getCleanFile(key, 1).exists());
-    assertFalse(getDirtyFile(key, 0).exists());
-    assertFalse(getDirtyFile(key, 1).exists());
-  }
-
-  private void assertValue(String key, String value0, String value1) throws Exception {
-    DiskLruCache.Snapshot snapshot = cache.get(key);
-    assertEquals(value0, snapshot.getString(0));
-    assertEquals(value1, snapshot.getString(1));
-    assertTrue(getCleanFile(key, 0).exists());
-    assertTrue(getCleanFile(key, 1).exists());
-    snapshot.close();
-  }
-}
diff --git a/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java b/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java
new file mode 100644
index 0000000..e933c17
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.IOException;
+import java.io.OutputStream;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.List;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class FaultRecoveringOutputStreamTest {
+  @Test public void noRecoveryWithoutReplacement() throws Exception {
+    FaultingOutputStream faulting = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting);
+
+    recovering.write('a');
+    faulting.nextFault = "system on fire";
+    try {
+      recovering.write('b');
+      fail();
+    } catch (IOException e) {
+      assertEquals(Arrays.asList("system on fire"), recovering.exceptionMessages);
+      assertEquals("ab", faulting.receivedUtf8);
+      assertFalse(faulting.closed);
+    }
+  }
+
+  @Test public void successfulRecoveryOnWriteFault() throws Exception {
+    FaultingOutputStream faulting1 = new FaultingOutputStream();
+    FaultingOutputStream faulting2 = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+    recovering.replacements.addLast(faulting2);
+
+    recovering.write('a');
+    assertEquals("a", faulting1.receivedUtf8);
+    assertEquals("", faulting2.receivedUtf8);
+    faulting1.nextFault = "system under water";
+    recovering.write('b');
+    assertEquals(Arrays.asList("system under water"), recovering.exceptionMessages);
+    assertEquals("ab", faulting1.receivedUtf8);
+    assertEquals("ab", faulting2.receivedUtf8);
+    assertTrue(faulting1.closed);
+    assertFalse(faulting2.closed);
+
+    // Confirm that new data goes to the new stream.
+    recovering.write('c');
+    assertEquals("ab", faulting1.receivedUtf8);
+    assertEquals("abc", faulting2.receivedUtf8);
+  }
+
+  @Test public void successfulRecoveryOnFlushFault() throws Exception {
+    FaultingOutputStream faulting1 = new FaultingOutputStream();
+    FaultingOutputStream faulting2 = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+    recovering.replacements.addLast(faulting2);
+
+    recovering.write('a');
+    faulting1.nextFault = "bad weather";
+    recovering.flush();
+    assertEquals(Arrays.asList("bad weather"), recovering.exceptionMessages);
+    assertEquals("a", faulting1.receivedUtf8);
+    assertEquals("a", faulting2.receivedUtf8);
+    assertTrue(faulting1.closed);
+    assertFalse(faulting2.closed);
+    assertEquals("a", faulting2.flushedUtf8);
+
+    // Confirm that new data goes to the new stream.
+    recovering.write('b');
+    assertEquals("a", faulting1.receivedUtf8);
+    assertEquals("ab", faulting2.receivedUtf8);
+    assertEquals("a", faulting2.flushedUtf8);
+  }
+
+  @Test public void successfulRecoveryOnCloseFault() throws Exception {
+    FaultingOutputStream faulting1 = new FaultingOutputStream();
+    FaultingOutputStream faulting2 = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+    recovering.replacements.addLast(faulting2);
+
+    recovering.write('a');
+    faulting1.nextFault = "termites";
+    recovering.close();
+    assertEquals(Arrays.asList("termites"), recovering.exceptionMessages);
+    assertEquals("a", faulting1.receivedUtf8);
+    assertEquals("a", faulting2.receivedUtf8);
+    assertTrue(faulting1.closed);
+    assertTrue(faulting2.closed);
+  }
+
+  @Test public void replacementStreamFaultsImmediately() throws Exception {
+    FaultingOutputStream faulting1 = new FaultingOutputStream();
+    FaultingOutputStream faulting2 = new FaultingOutputStream();
+    FaultingOutputStream faulting3 = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+    recovering.replacements.addLast(faulting2);
+    recovering.replacements.addLast(faulting3);
+
+    recovering.write('a');
+    assertEquals("a", faulting1.receivedUtf8);
+    assertEquals("", faulting2.receivedUtf8);
+    assertEquals("", faulting3.receivedUtf8);
+    faulting1.nextFault = "offline";
+    faulting2.nextFault = "slow";
+    recovering.write('b');
+    assertEquals(Arrays.asList("offline", "slow"), recovering.exceptionMessages);
+    assertEquals("ab", faulting1.receivedUtf8);
+    assertEquals("a", faulting2.receivedUtf8);
+    assertEquals("ab", faulting3.receivedUtf8);
+    assertTrue(faulting1.closed);
+    assertTrue(faulting2.closed);
+    assertFalse(faulting3.closed);
+
+    // Confirm that new data goes to the new stream.
+    recovering.write('c');
+    assertEquals("ab", faulting1.receivedUtf8);
+    assertEquals("a", faulting2.receivedUtf8);
+    assertEquals("abc", faulting3.receivedUtf8);
+  }
+
+  @Test public void recoverWithFullBuffer() throws Exception {
+    FaultingOutputStream faulting1 = new FaultingOutputStream();
+    FaultingOutputStream faulting2 = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+    recovering.replacements.addLast(faulting2);
+
+    recovering.write("abcdefghij".getBytes(UTF_8)); // 10 bytes.
+    faulting1.nextFault = "unlucky";
+    recovering.write('k');
+    assertEquals("abcdefghijk", faulting1.receivedUtf8);
+    assertEquals("abcdefghijk", faulting2.receivedUtf8);
+    assertEquals(Arrays.asList("unlucky"), recovering.exceptionMessages);
+    assertTrue(faulting1.closed);
+    assertFalse(faulting2.closed);
+
+    // Confirm that new data goes to the new stream.
+    recovering.write('l');
+    assertEquals("abcdefghijk", faulting1.receivedUtf8);
+    assertEquals("abcdefghijkl", faulting2.receivedUtf8);
+  }
+
+  @Test public void noRecoveryWithOverfullBuffer() throws Exception {
+    FaultingOutputStream faulting1 = new FaultingOutputStream();
+    FaultingOutputStream faulting2 = new FaultingOutputStream();
+    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+    recovering.replacements.addLast(faulting2);
+
+    recovering.write("abcdefghijk".getBytes(UTF_8)); // 11 bytes.
+    faulting1.nextFault = "out to lunch";
+    try {
+      recovering.write('l');
+      fail();
+    } catch (IOException expected) {
+      assertEquals("out to lunch", expected.getMessage());
+    }
+
+    assertEquals(Arrays.<String>asList(), recovering.exceptionMessages);
+    assertEquals("abcdefghijkl", faulting1.receivedUtf8);
+    assertEquals("", faulting2.receivedUtf8);
+    assertFalse(faulting1.closed);
+    assertFalse(faulting2.closed);
+  }
+
+  static class FaultingOutputStream extends OutputStream {
+    String receivedUtf8 = "";
+    String flushedUtf8 = null;
+    String nextFault;
+    boolean closed;
+
+    @Override public final void write(int data) throws IOException {
+      write(new byte[] { (byte) data });
+    }
+
+    @Override public void write(byte[] buffer, int offset, int count) throws IOException {
+      receivedUtf8 += new String(buffer, offset, count, UTF_8);
+      if (nextFault != null) throw new IOException(nextFault);
+    }
+
+    @Override public void flush() throws IOException {
+      flushedUtf8 = receivedUtf8;
+      if (nextFault != null) throw new IOException(nextFault);
+    }
+
+    @Override public void close() throws IOException {
+      closed = true;
+      if (nextFault != null) throw new IOException(nextFault);
+    }
+  }
+
+  static class TestFaultRecoveringOutputStream extends FaultRecoveringOutputStream {
+    final List<String> exceptionMessages = new ArrayList<String>();
+    final Deque<OutputStream> replacements = new ArrayDeque<OutputStream>();
+
+    TestFaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream first) {
+      super(maxReplayBufferLength, first);
+    }
+
+    @Override protected OutputStream replacementStream(IOException e) {
+      exceptionMessages.add(e.getMessage());
+      return replacements.poll();
+    }
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java b/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
new file mode 100644
index 0000000..636acbd
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.squareup.okhttp.OkAuthenticator;
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class RecordingOkAuthenticator implements OkAuthenticator {
+  public final List<String> calls = new ArrayList<String>();
+  public final Credential credential;
+
+  public RecordingOkAuthenticator(Credential credential) {
+    this.credential = credential;
+  }
+
+  @Override public Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges)
+      throws IOException {
+    calls.add("authenticate"
+        + " proxy=" + proxy.type()
+        + " url=" + url
+        + " challenges=" + challenges);
+    return credential;
+  }
+
+  @Override public Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges)
+      throws IOException {
+    calls.add("authenticateProxy"
+        + " proxy=" + proxy.type()
+        + " url=" + url
+        + " challenges=" + challenges);
+    return credential;
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
index e54ed21..64a4738 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
@@ -19,7 +19,9 @@
 import com.google.mockwebserver.MockResponse;
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.ResponseSource;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -112,7 +114,7 @@
   @After public void tearDown() throws Exception {
     server.shutdown();
     ResponseCache.setDefault(null);
-    cache.getCache().delete();
+    cache.delete();
     CookieHandler.setDefault(null);
   }
 
@@ -970,7 +972,7 @@
     assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
     URLConnection connection = openConnection(server.getUrl("/"));
     connection.addRequestProperty("Cache-Control", "only-if-cached");
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals("A", readAscii(connection));
   }
 
   @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
@@ -1584,6 +1586,74 @@
     assertEquals(2, server.getRequestCount());
   }
 
+  @Test public void responseSourceHeaderCached() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=30")
+        .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "only-if-cached");
+    assertEquals("A", readAscii(connection));
+
+    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CACHE.toString() + " 200", source);
+  }
+
+  @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=30")
+        .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES)));
+    server.enqueue(new MockResponse().setBody("B")
+        .addHeader("Cache-Control: max-age=30")
+        .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    assertEquals("B", readAscii(connection));
+
+    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 200", source);
+  }
+
+  @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=0")
+        .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+    server.enqueue(new MockResponse().setResponseCode(304));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection));
+
+    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 304", source);
+  }
+
+  @Test public void responseSourceHeaderFetched() throws IOException {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection));
+
+    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.NETWORK.toString() + " 200", source);
+  }
+
+  @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
+    server.enqueue(new MockResponse()
+        .addHeader("Cache-Control: max-age=120")
+        .addHeader(": A")
+        .setBody("body"));
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("A", connection.getHeaderField(""));
+  }
+
   /**
    * @param delta the offset from the current date to use. Negative
    * values yield dates in the past; positive values yield dates in the
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
index e5629f0..474e507 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
@@ -21,7 +21,6 @@
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 public final class RawHeadersTest {
   @Test public void parseNameValueBlock() {
@@ -80,7 +79,7 @@
     final int version = 1;
     final int code = 503;
     rawHeaders.setStatusLine("HTTP/1." + version + " " + code + " ");
-    assertTrue(rawHeaders.getResponseMessage().isEmpty());
+    assertEquals("", rawHeaders.getResponseMessage());
     assertEquals(version, rawHeaders.getHttpMinorVersion());
     assertEquals(code, rawHeaders.getResponseCode());
   }
@@ -95,7 +94,7 @@
     final int version = 1;
     final int code = 503;
     rawHeaders.setStatusLine("HTTP/1." + version + " " + code);
-    assertTrue(rawHeaders.getResponseMessage().isEmpty());
+    assertEquals("", rawHeaders.getResponseMessage());
     assertEquals(version, rawHeaders.getHttpMinorVersion());
     assertEquals(code, rawHeaders.getResponseCode());
   }
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index 4dc2e32..687a397 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -18,6 +18,8 @@
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.OkAuthenticator;
+import com.squareup.okhttp.Route;
 import com.squareup.okhttp.internal.Dns;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.IOException;
@@ -30,10 +32,14 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.Set;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSocketFactory;
 import org.junit.Test;
 
@@ -60,6 +66,7 @@
   private static final SSLSocketFactory socketFactory;
   private static final HostnameVerifier hostnameVerifier;
   private static final ConnectionPool pool;
+
   static {
     try {
       uri = new URI("http://" + uriHost + ":" + uriPort + "/path");
@@ -72,12 +79,15 @@
     }
   }
 
+  private final OkAuthenticator authenticator = HttpAuthenticator.SYSTEM_DEFAULT;
+  private final List<String> transports = Arrays.asList("http/1.1");
   private final FakeDns dns = new FakeDns();
   private final FakeProxySelector proxySelector = new FakeProxySelector();
 
   @Test public void singleRoute() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, null);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
 
     assertTrue(routeSelector.hasNext());
     dns.inetAddresses = makeFakeAddresses(255, 1);
@@ -92,9 +102,31 @@
     }
   }
 
+  @Test public void singleRouteReturnsFailedRoute() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    Connection connection = routeSelector.next();
+    Set<Route> failedRoutes = new LinkedHashSet<Route>();
+    failedRoutes.add(connection.getRoute());
+    routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    assertFalse(routeSelector.hasNext());
+    try {
+      routeSelector.next();
+      fail();
+    } catch (NoSuchElementException expected) {
+    }
+  }
+
   @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, proxyA);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA, transports);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
 
     assertTrue(routeSelector.hasNext());
     dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -109,8 +141,10 @@
   }
 
   @Test public void explicitDirectProxy() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, NO_PROXY);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY,
+        transports);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
 
     assertTrue(routeSelector.hasNext());
     dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -123,10 +157,11 @@
   }
 
   @Test public void proxySelectorReturnsNull() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, null);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
 
     proxySelector.proxies = null;
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
     proxySelector.assertRequests(uri);
 
     assertTrue(routeSelector.hasNext());
@@ -138,8 +173,9 @@
   }
 
   @Test public void proxySelectorReturnsNoProxies() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, null);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
 
     assertTrue(routeSelector.hasNext());
     dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -152,11 +188,12 @@
   }
 
   @Test public void proxySelectorReturnsMultipleProxies() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, null);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
 
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
     proxySelector.assertRequests(uri);
 
     // First try the IP addresses of the first proxy, in sequence.
@@ -185,10 +222,11 @@
   }
 
   @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, null);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
 
     proxySelector.proxies.add(NO_PROXY);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
     proxySelector.assertRequests(uri);
 
     // Only the origin server will be attempted.
@@ -201,12 +239,13 @@
   }
 
   @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, null);
+    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
 
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
     proxySelector.proxies.add(proxyA);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
     proxySelector.assertRequests(uri);
 
     assertTrue(routeSelector.hasNext());
@@ -238,28 +277,39 @@
     assertFalse(routeSelector.hasNext());
   }
 
-  @Test public void multipleTlsModes() throws Exception {
-    Address address =
-        new Address(uriHost, uriPort, socketFactory, hostnameVerifier, Proxy.NO_PROXY);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+  @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception {
+    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+        Proxy.NO_PROXY, transports);
+    Set<Route> failedRoutes = new LinkedHashSet<Route>();
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        failedRoutes);
 
-    assertTrue(routeSelector.hasNext());
     dns.inetAddresses = makeFakeAddresses(255, 1);
-    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, true);
-    dns.assertRequests(uriHost);
+    Connection connection = routeSelector.next();
+    routeSelector.connectFailed(connection, new IOException("Non SSL exception"));
+    assertTrue(failedRoutes.size() == 2);
+  }
 
-    assertTrue(routeSelector.hasNext());
-    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-    dns.assertRequests(); // No more DNS requests since the previous!
+  @Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception {
+    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+        Proxy.NO_PROXY, transports);
+    Set<Route> failedRoutes = new LinkedHashSet<Route>();
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        failedRoutes);
 
-    assertFalse(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    Connection connection = routeSelector.next();
+    routeSelector.connectFailed(connection, new SSLHandshakeException("SSL exception"));
+    assertTrue(failedRoutes.size() == 1);
   }
 
   @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, null);
+    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+        null, transports);
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
-    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        Collections.EMPTY_SET);
 
     // Proxy A
     dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -292,13 +342,45 @@
     assertFalse(routeSelector.hasNext());
   }
 
+  @Test public void failedRoutesAreLast() throws Exception {
+    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+        Proxy.NO_PROXY, transports);
+
+    Set<Route> failedRoutes = new LinkedHashSet<Route>(1);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+        failedRoutes);
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+
+    // Extract the regular sequence of routes from selector.
+    List<Connection> regularRoutes = new ArrayList<Connection>();
+    while (routeSelector.hasNext()) {
+      regularRoutes.add(routeSelector.next());
+    }
+
+    // Check that we do indeed have more than one route.
+    assertTrue(regularRoutes.size() > 1);
+    // Add first regular route as failed.
+    failedRoutes.add(regularRoutes.get(0).getRoute());
+    // Reset selector
+    routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, failedRoutes);
+
+    List<Connection> routesWithFailedRoute = new ArrayList<Connection>();
+    while (routeSelector.hasNext()) {
+      routesWithFailedRoute.add(routeSelector.next());
+    }
+
+    assertEquals(regularRoutes.get(0).getRoute(),
+        routesWithFailedRoute.get(routesWithFailedRoute.size() - 1).getRoute());
+    assertEquals(regularRoutes.size(), routesWithFailedRoute.size());
+  }
+
   private void assertConnection(Connection connection, Address address, Proxy proxy,
       InetAddress socketAddress, int socketPort, boolean modernTls) {
-    assertEquals(address, connection.getAddress());
-    assertEquals(proxy, connection.getProxy());
-    assertEquals(socketAddress, connection.getSocketAddress().getAddress());
-    assertEquals(socketPort, connection.getSocketAddress().getPort());
-    assertEquals(modernTls, connection.isModernTls());
+    assertEquals(address, connection.getRoute().getAddress());
+    assertEquals(proxy, connection.getRoute().getProxy());
+    assertEquals(socketAddress, connection.getRoute().getSocketAddress().getAddress());
+    assertEquals(socketPort, connection.getRoute().getSocketAddress().getPort());
+    assertEquals(modernTls, connection.getRoute().isModernTls());
   }
 
   private static InetAddress[] makeFakeAddresses(int prefix, int count) {
diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 9a3572a..7f66519 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -20,9 +20,11 @@
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
 import com.google.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.internal.RecordingAuthenticator;
 import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.RecordingOkAuthenticator;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -56,6 +58,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -77,6 +80,7 @@
 import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
 import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
 import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
+import static com.squareup.okhttp.OkAuthenticator.Credential;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -117,7 +121,7 @@
     server.shutdown();
     server2.shutdown();
     if (cache != null) {
-      cache.getCache().delete();
+      cache.delete();
     }
   }
 
@@ -153,9 +157,17 @@
       fail();
     } catch (NullPointerException expected) {
     }
-    urlConnection.setRequestProperty("NullValue", null); // should fail silently!
+    try {
+      urlConnection.setRequestProperty("NullValue", null);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
     assertNull(urlConnection.getRequestProperty("NullValue"));
-    urlConnection.addRequestProperty("AnotherNullValue", null);  // should fail silently!
+    try {
+      urlConnection.addRequestProperty("AnotherNullValue", null);
+      fail();
+    } catch (Exception expected) {
+    }
     assertNull(urlConnection.getRequestProperty("AnotherNullValue"));
 
     urlConnection.getResponseCode();
@@ -507,7 +519,7 @@
     server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
     server.play();
 
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(server.getUrl("/foo"));
 
@@ -527,7 +539,7 @@
     SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
     RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    client.setSSLSocketFactory(clientSocketFactory);
+    client.setSslSocketFactory(clientSocketFactory);
     client.setHostnameVerifier(hostnameVerifier);
     HttpURLConnection connection = client.open(server.getUrl("/"));
     assertContent("this response comes via HTTPS", connection);
@@ -547,12 +559,12 @@
     server.play();
 
     // install a custom SSL socket factory so the server can be authorized
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection1 = client.open(server.getUrl("/"));
     assertContent("this response comes via HTTPS", connection1);
 
-    client.setSSLSocketFactory(null);
+    client.setSslSocketFactory(null);
     HttpURLConnection connection2 = client.open(server.getUrl("/"));
     try {
       readAscii(connection2.getInputStream(), Integer.MAX_VALUE);
@@ -567,7 +579,7 @@
     server.enqueue(new MockResponse().setBody("this response comes via SSL"));
     server.play();
 
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(server.getUrl("/foo"));
 
@@ -663,7 +675,7 @@
     server.play();
 
     URL url = server.getUrl("/foo");
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = proxyConfig.connect(server, client, url);
 
@@ -703,7 +715,7 @@
     server.play();
 
     URL url = new URL("https://android.com/foo");
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(hostnameVerifier);
     HttpURLConnection connection = proxyConfig.connect(server, client, url);
 
@@ -740,7 +752,7 @@
     client.setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     HttpURLConnection connection = client.open(url);
 
     try {
@@ -778,7 +790,7 @@
     client.setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(hostnameVerifier);
     HttpURLConnection connection = client.open(url);
     connection.addRequestProperty("Private", "Secret");
@@ -810,7 +822,7 @@
     client.setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(url);
     assertContent("A", connection);
@@ -840,7 +852,7 @@
     client.setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(url);
     connection.setRequestProperty("Connection", "close");
@@ -861,7 +873,7 @@
     client.setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
-    client.setSSLSocketFactory(socketFactory);
+    client.setSslSocketFactory(socketFactory);
     client.setHostnameVerifier(hostnameVerifier);
     assertContent("response 1", client.open(url));
     assertContent("response 2", client.open(url));
@@ -1076,7 +1088,7 @@
       SSLSocketFactory socketFactory = sslContext.getSocketFactory();
       RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
       server.useHttps(socketFactory, false);
-      client.setSSLSocketFactory(socketFactory);
+      client.setSslSocketFactory(socketFactory);
       client.setHostnameVerifier(hostnameVerifier);
     }
 
@@ -1387,7 +1399,7 @@
     server.enqueue(new MockResponse().setBody("Success!"));
     server.play();
 
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
@@ -1521,7 +1533,7 @@
     server.enqueue(new MockResponse().setBody("This is the new location!"));
     server.play();
 
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(server.getUrl("/"));
     assertEquals("This is the new location!",
@@ -1542,7 +1554,7 @@
     server.play();
 
     client.setFollowProtocolRedirects(false);
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     HttpURLConnection connection = client.open(server.getUrl("/"));
     assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
@@ -1570,7 +1582,7 @@
         .setBody("This page has moved!"));
     server.play();
 
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     client.setFollowProtocolRedirects(true);
     HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
@@ -1593,7 +1605,7 @@
         .setBody("This page has moved!"));
     server.play();
 
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     client.setFollowProtocolRedirects(true);
     HttpURLConnection connection = client.open(server.getUrl("/"));
@@ -1614,7 +1626,7 @@
     if (https) {
       server.useHttps(sslContext.getSocketFactory(), false);
       server2.useHttps(sslContext.getSocketFactory(), false);
-      client.setSSLSocketFactory(sslContext.getSocketFactory());
+      client.setSslSocketFactory(sslContext.getSocketFactory());
       client.setHostnameVerifier(new RecordingHostnameVerifier());
     }
 
@@ -1731,6 +1743,70 @@
     assertEquals(1, server.getRequestCount());
   }
 
+  @Test public void response307WithGet() throws Exception {
+    test307Redirect("GET");
+  }
+
+  @Test public void response307WithHead() throws Exception {
+    test307Redirect("HEAD");
+  }
+
+  @Test public void response307WithOptions() throws Exception {
+    test307Redirect("OPTIONS");
+  }
+
+  @Test public void response307WithPost() throws Exception {
+    test307Redirect("POST");
+  }
+
+  private void test307Redirect(String method) throws Exception {
+    MockResponse response1 = new MockResponse()
+        .setResponseCode(HttpURLConnectionImpl.HTTP_TEMP_REDIRECT)
+        .addHeader("Location: /page2");
+    if (!method.equals("HEAD")) {
+      response1.setBody("This page has moved!");
+    }
+    server.enqueue(response1);
+    server.enqueue(new MockResponse().setBody("Page 2"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/page1"));
+    connection.setRequestMethod(method);
+    byte[] requestBody = { 'A', 'B', 'C', 'D' };
+    if (method.equals("POST")) {
+      connection.setDoOutput(true);
+      OutputStream outputStream = connection.getOutputStream();
+      outputStream.write(requestBody);
+      outputStream.close();
+    }
+
+    String response = readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+
+    RecordedRequest page1 = server.takeRequest();
+    assertEquals(method + " /page1 HTTP/1.1", page1.getRequestLine());
+
+    if (method.equals("GET")) {
+        assertEquals("Page 2", response);
+    } else if (method.equals("HEAD"))  {
+        assertEquals("", response);
+    } else {
+      // Methods other than GET/HEAD shouldn't follow the redirect
+      if (method.equals("POST")) {
+        assertTrue(connection.getDoOutput());
+        assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
+      }
+      assertEquals(1, server.getRequestCount());
+      assertEquals("This page has moved!", response);
+      return;
+    }
+
+    // GET/HEAD requests should have followed the redirect with the same method
+    assertFalse(connection.getDoOutput());
+    assertEquals(2, server.getRequestCount());
+    RecordedRequest page2 = server.takeRequest();
+    assertEquals(method + " /page2 HTTP/1.1", page2.getRequestLine());
+  }
+
   @Test public void follow20Redirects() throws Exception {
     for (int i = 0; i < 20; i++) {
       server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
@@ -1772,7 +1848,7 @@
     sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
 
     client.setHostnameVerifier(hostnameVerifier);
-    client.setSSLSocketFactory(sc.getSocketFactory());
+    client.setSslSocketFactory(sc.getSocketFactory());
     server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("ABC"));
     server.enqueue(new MockResponse().setBody("DEF"));
@@ -2213,6 +2289,97 @@
     assertEquals(-1, in.read());
   }
 
+  @Test public void postFailsWithBufferedRequestForSmallRequest() throws Exception {
+    reusedConnectionFailsWithPost(TransferKind.END_OF_STREAM, 1024);
+  }
+
+  // This test is ignored because we don't (yet) reliably recover for large request bodies.
+  @Test @Ignore public void postFailsWithBufferedRequestForLargeRequest() throws Exception {
+    reusedConnectionFailsWithPost(TransferKind.END_OF_STREAM, 16384);
+  }
+
+  @Test public void postFailsWithChunkedRequestForSmallRequest() throws Exception {
+    reusedConnectionFailsWithPost(TransferKind.CHUNKED, 1024);
+  }
+
+  // This test is ignored because we don't (yet) reliably recover for large request bodies.
+  @Test @Ignore public void postFailsWithChunkedRequestForLargeRequest() throws Exception {
+    reusedConnectionFailsWithPost(TransferKind.CHUNKED, 16384);
+  }
+
+  @Test public void postFailsWithFixedLengthRequestForSmallRequest() throws Exception {
+    reusedConnectionFailsWithPost(TransferKind.FIXED_LENGTH, 1024);
+  }
+
+  // This test is ignored because we don't (yet) reliably recover for large request bodies.
+  @Test @Ignore public void postFailsWithFixedLengthRequestForLargeRequest() throws Exception {
+    reusedConnectionFailsWithPost(TransferKind.FIXED_LENGTH, 16384);
+  }
+
+  private void reusedConnectionFailsWithPost(TransferKind transferKind, int requestSize)
+      throws Exception {
+    server.enqueue(new MockResponse().setBody("A").setSocketPolicy(SHUTDOWN_INPUT_AT_END));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    assertContent("A", client.open(server.getUrl("/a")));
+
+    // If the request body is larger than OkHttp's replay buffer, the failure may still occur.
+    byte[] requestBody = new byte[requestSize];
+    new Random(0).nextBytes(requestBody);
+
+    HttpURLConnection connection = client.open(server.getUrl("/b"));
+    connection.setRequestMethod("POST");
+    transferKind.setForRequest(connection, requestBody.length);
+    for (int i = 0; i < requestBody.length; i += 1024) {
+      connection.getOutputStream().write(requestBody, i, 1024);
+    }
+    connection.getOutputStream().close();
+    assertContent("B", connection);
+
+    RecordedRequest requestA = server.takeRequest();
+    assertEquals("/a", requestA.getPath());
+    RecordedRequest requestB = server.takeRequest();
+    assertEquals("/b", requestB.getPath());
+    assertEquals(Arrays.toString(requestBody), Arrays.toString(requestB.getBody()));
+  }
+
+  @Test public void fullyBufferedPostIsTooShort() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/b"));
+    connection.setRequestProperty("Content-Length", "4");
+    connection.setRequestMethod("POST");
+    OutputStream out = connection.getOutputStream();
+    out.write('a');
+    out.write('b');
+    out.write('c');
+    try {
+      out.close();
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test public void fullyBufferedPostIsTooLong() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/b"));
+    connection.setRequestProperty("Content-Length", "3");
+    connection.setRequestMethod("POST");
+    OutputStream out = connection.getOutputStream();
+    out.write('a');
+    out.write('b');
+    out.write('c');
+    try {
+      out.write('d');
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
   @Test @Ignore public void testPooledConnectionsDetectHttp10() {
     // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1)
     fail("TODO");
@@ -2237,14 +2404,40 @@
     fail("TODO");
   }
 
-  @Test @Ignore public void emptyHeaderName() {
-    // This is relevant for SPDY
-    fail("TODO");
+  @Test public void emptyRequestHeaderValueIsAllowed() throws Exception {
+    server.enqueue(new MockResponse().setBody("body"));
+    server.play();
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    urlConnection.addRequestProperty("B", "");
+    assertContent("body", urlConnection);
+    assertEquals("", urlConnection.getRequestProperty("B"));
   }
 
-  @Test @Ignore public void emptyHeaderValue() {
-    // This is relevant for SPDY
-    fail("TODO");
+  @Test public void emptyResponseHeaderValueIsAllowed() throws Exception {
+    server.enqueue(new MockResponse().addHeader("A:").setBody("body"));
+    server.play();
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    assertContent("body", urlConnection);
+    assertEquals("", urlConnection.getHeaderField("A"));
+  }
+
+  @Test public void emptyRequestHeaderNameIsStrict() throws Exception {
+    server.enqueue(new MockResponse().setBody("body"));
+    server.play();
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    try {
+      urlConnection.setRequestProperty("", "A");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void emptyResponseHeaderNameIsLenient() throws Exception {
+    server.enqueue(new MockResponse().addHeader(":A").setBody("body"));
+    server.play();
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    urlConnection.getResponseCode();
+    assertEquals("A", urlConnection.getHeaderField(""));
   }
 
   @Test @Ignore public void deflateCompression() {
@@ -2259,6 +2452,53 @@
     fail("TODO");
   }
 
+  @Test public void customAuthenticator() throws Exception {
+    MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401)
+        .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+        .setBody("Please authenticate.");
+    server.enqueue(pleaseAuthenticate);
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    Credential credential = Credential.basic("jesse", "peanutbutter");
+    RecordingOkAuthenticator authenticator = new RecordingOkAuthenticator(credential);
+    client.setAuthenticator(authenticator);
+    assertContent("A", client.open(server.getUrl("/private")));
+
+    assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*");
+    assertContains(server.takeRequest().getHeaders(),
+        "Authorization: " + credential.getHeaderValue());
+
+    assertEquals(1, authenticator.calls.size());
+    String call = authenticator.calls.get(0);
+    assertTrue(call, call.contains("proxy=DIRECT"));
+    assertTrue(call, call.contains("url=" + server.getUrl("/private")));
+    assertTrue(call, call.contains("challenges=[Basic realm=\"protected area\"]"));
+  }
+
+  @Test public void setTransports() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+    client.setTransports(Arrays.asList("http/1.1"));
+    assertContent("A", client.open(server.getUrl("/")));
+  }
+
+  @Test public void setTransportsWithoutHttp11() throws Exception {
+    try {
+      client.setTransports(Arrays.asList("spdy/3"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setTransportsWithNull() throws Exception {
+    try {
+      client.setTransports(Arrays.asList("http/1.1", null));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
   /** Returns a gzipped copy of {@code bytes}. */
   public byte[] gzip(byte[] bytes) throws IOException {
     ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
@@ -2314,7 +2554,7 @@
         response.setBody(content);
       }
       @Override void setForRequest(HttpURLConnection connection, int contentLength) {
-        connection.setChunkedStreamingMode(contentLength);
+        connection.setFixedLengthStreamingMode(contentLength);
       }
     },
     END_OF_STREAM() {
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
index b8afeb2..5970088 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
@@ -17,11 +17,11 @@
 
 import com.google.mockwebserver.MockResponse;
 import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.internal.RecordingAuthenticator;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.HttpResponseCache;
 import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -78,7 +78,7 @@
   private HttpResponseCache cache;
 
   @Before public void setUp() throws Exception {
-    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
     String systemTmpDir = System.getProperty("java.io.tmpdir");
     File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID());
diff --git a/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java b/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
new file mode 100644
index 0000000..215e968
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
@@ -0,0 +1,120 @@
+/*
+ * 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.internal.tls;
+
+import java.security.Principal;
+import java.security.cert.Certificate;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSessionContext;
+import javax.security.cert.X509Certificate;
+
+final class FakeSSLSession implements SSLSession {
+  private final Certificate[] certificates;
+
+  public FakeSSLSession(Certificate... certificates) throws Exception {
+    this.certificates = certificates;
+  }
+
+  public int getApplicationBufferSize() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getCipherSuite() {
+    throw new UnsupportedOperationException();
+  }
+
+  public long getCreationTime() {
+    throw new UnsupportedOperationException();
+  }
+
+  public byte[] getId() {
+    throw new UnsupportedOperationException();
+  }
+
+  public long getLastAccessedTime() {
+    throw new UnsupportedOperationException();
+  }
+
+  public Certificate[] getLocalCertificates() {
+    throw new UnsupportedOperationException();
+  }
+
+  public Principal getLocalPrincipal() {
+    throw new UnsupportedOperationException();
+  }
+
+  public int getPacketBufferSize() {
+    throw new UnsupportedOperationException();
+  }
+
+  public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
+    if (certificates.length == 0) {
+      throw new SSLPeerUnverifiedException("peer not authenticated");
+    } else {
+      return certificates;
+    }
+  }
+
+  public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getPeerHost() {
+    throw new UnsupportedOperationException();
+  }
+
+  public int getPeerPort() {
+    throw new UnsupportedOperationException();
+  }
+
+  public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getProtocol() {
+    throw new UnsupportedOperationException();
+  }
+
+  public SSLSessionContext getSessionContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  public void putValue(String s, Object obj) {
+    throw new UnsupportedOperationException();
+  }
+
+  public void removeValue(String s) {
+    throw new UnsupportedOperationException();
+  }
+
+  public Object getValue(String s) {
+    throw new UnsupportedOperationException();
+  }
+
+  public String[] getValueNames() {
+    throw new UnsupportedOperationException();
+  }
+
+  public void invalidate() {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean isValid() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java b/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
new file mode 100644
index 0000000..f908fb8
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
@@ -0,0 +1,553 @@
+/*
+ * 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.internal.tls;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for our hostname verifier. Most of these tests are from AOSP, which
+ * itself includes tests from the Apache HTTP Client test suite.
+ */
+public final class HostnameVerifierTest {
+  private HostnameVerifier verifier = new OkHostnameVerifier();
+
+  @Test public void verify() throws Exception {
+    FakeSSLSession session = new FakeSSLSession();
+    assertFalse(verifier.verify("localhost", session));
+  }
+
+  @Test public void verifyCn() throws Exception {
+    // CN=foo.com
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aQMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzE0MVoXDTI4MTEwNTE1MzE0MVowgaQx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs\n"
+        + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+        + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+        + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+        + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+        + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+        + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+        + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB\n"
+        + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE\n"
+        + "FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS\n"
+        + "yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQC3jRmEya6sQCkmieULcvx8zz1euCk9\n"
+        + "fSez7BEtki8+dmfMXe3K7sH0lI8f4jJR0rbSCjpmCQLYmzC3NxBKeJOW0RcjNBpO\n"
+        + "c2JlGO9auXv2GDP4IYiXElLJ6VSqc8WvDikv0JmCCWm0Zga+bZbR/EWN5DeEtFdF\n"
+        + "815CLpJZNcYwiYwGy/CVQ7w2TnXlG+mraZOz+owr+cL6J/ZesbdEWfjoS1+cUEhE\n"
+        + "HwlNrAu8jlZ2UqSgskSWlhYdMTAP9CPHiUv9N7FcT58Itv/I4fKREINQYjDpvQcx\n"
+        + "SaTYb9dr5sB4WLNglk7zxDtM80H518VvihTcP7FHL+Gn6g4j5fkI98+S\n"
+        + "-----END CERTIFICATE-----\n");
+    assertTrue(verifier.verify("foo.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    assertFalse(verifier.verify("bar.com", session));
+  }
+
+  @Test public void verifyNonAsciiCn() throws Exception {
+    // CN=&#x82b1;&#x5b50;.co.jp
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIESzCCAzOgAwIBAgIJAIz+EYMBU6aTMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1NDIxNVoXDTI4MTEwNTE1NDIxNVowgakx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczEVMBMGA1UEAwwM6Iqx5a2QLmNvLmpwMSUwIwYJKoZIhvcNAQkB\n"
+        + "FhZqdWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n"
+        + "MIIBCgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjU\n"
+        + "g4pNjYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQc\n"
+        + "wHf0ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t\n"
+        + "7iu1JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAn\n"
+        + "AxK6q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArD\n"
+        + "qUYxqJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG\n"
+        + "CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV\n"
+        + "HQ4EFgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLS\n"
+        + "rNuzA1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBALJ27i3okV/KvlDp6KMID3gd\n"
+        + "ITl68PyItzzx+SquF8gahMh016NX73z/oVZoVUNdftla8wPUB1GwIkAnGkhQ9LHK\n"
+        + "spBdbRiCj0gMmLCsX8SrjFvr7cYb2cK6J/fJe92l1tg/7Y4o7V/s4JBe/cy9U9w8\n"
+        + "a0ctuDmEBCgC784JMDtT67klRfr/2LlqWhlOEq7pUFxRLbhpquaAHSOjmIcWnVpw\n"
+        + "9BsO7qe46hidgn39hKh1WjKK2VcL/3YRsC4wUi0PBtFW6ScMCuMhgIRXSPU55Rae\n"
+        + "UIlOdPjjr1SUNWGId1rD7W16Scpwnknn310FNxFMHVI0GTGFkNdkilNCFJcIoRA=\n"
+        + "-----END CERTIFICATE-----\n");
+    assertTrue(verifier.verify("\u82b1\u5b50.co.jp", session));
+    assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
+  }
+
+  @Test public void verifySubjectAlt() throws Exception {
+    // CN=foo.com, subjectAlt=bar.com
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIEXDCCA0SgAwIBAgIJAIz+EYMBU6aRMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzYyOVoXDTI4MTEwNTE1MzYyOVowgaQx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs\n"
+        + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+        + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+        + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+        + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+        + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+        + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+        + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCG\n"
+        + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+        + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+        + "A1LKh6YNPg0wEgYDVR0RBAswCYIHYmFyLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEA\n"
+        + "dQyprNZBmVnvuVWjV42sey/PTfkYShJwy1j0/jcFZR/ypZUovpiHGDO1DgL3Y3IP\n"
+        + "zVQ26uhUsSw6G0gGRiaBDe/0LUclXZoJzXX1qpS55OadxW73brziS0sxRgGrZE/d\n"
+        + "3g5kkio6IED47OP6wYnlmZ7EKP9cqjWwlnvHnnUcZ2SscoLNYs9rN9ccp8tuq2by\n"
+        + "88OyhKwGjJfhOudqfTNZcDzRHx4Fzm7UsVaycVw4uDmhEHJrAsmMPpj/+XRK9/42\n"
+        + "2xq+8bc6HojdtbCyug/fvBZvZqQXSmU8m8IVcMmWMz0ZQO8ee3QkBHMZfCy7P/kr\n"
+        + "VbWx/uETImUu+NZg22ewEw==\n"
+        + "-----END CERTIFICATE-----\n");
+    assertFalse(verifier.verify("foo.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    assertTrue(verifier.verify("bar.com", session));
+    assertFalse(verifier.verify("a.bar.com", session));
+  }
+
+  /**
+   * Ignored due to incompatibilities between Android and Java on how non-ASCII
+   * subject alt names are parsed. Android fails to parse these, which means we
+   * fall back to the CN. The RI does parse them, so the CN is unused.
+   */
+  @Test @Ignore public void verifyNonAsciiSubjectAlt() throws Exception {
+    // CN=foo.com, subjectAlt=bar.com, subjectAlt=&#x82b1;&#x5b50;.co.jp
+    // (hanako.co.jp in kanji)
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIEajCCA1KgAwIBAgIJAIz+EYMBU6aSMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzgxM1oXDTI4MTEwNTE1MzgxM1owgaQx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs\n"
+        + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+        + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+        + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+        + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+        + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+        + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+        + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaOBnjCBmzAJBgNVHRMEAjAAMCwGCWCG\n"
+        + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+        + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+        + "A1LKh6YNPg0wIAYDVR0RBBkwF4IHYmFyLmNvbYIM6Iqx5a2QLmNvLmpwMA0GCSqG\n"
+        + "SIb3DQEBBQUAA4IBAQBeZs7ZIYyKtdnVxVvdLgwySEPOE4pBSXii7XYv0Q9QUvG/\n"
+        + "++gFGQh89HhABzA1mVUjH5dJTQqSLFvRfqTHqLpxSxSWqMHnvRM4cPBkIRp/XlMK\n"
+        + "PlXadYtJLPTgpbgvulA1ickC9EwlNYWnowZ4uxnfsMghW4HskBqaV+PnQ8Zvy3L0\n"
+        + "12c7Cg4mKKS5pb1HdRuiD2opZ+Hc77gRQLvtWNS8jQvd/iTbh6fuvTKfAOFoXw22\n"
+        + "sWIKHYrmhCIRshUNohGXv50m2o+1w9oWmQ6Dkq7lCjfXfUB4wIbggJjpyEtbNqBt\n"
+        + "j4MC2x5rfsLKKqToKmNE7pFEgqwe8//Aar1b+Qj+\n"
+        + "-----END CERTIFICATE-----\n");
+    assertTrue(verifier.verify("foo.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    // these checks test alternative subjects. The test data contains an
+    // alternative subject starting with a japanese kanji character. This is
+    // not supported by Android because the underlying implementation from
+    // harmony follows the definition from rfc 1034 page 10 for alternative
+    // subject names. This causes the code to drop all alternative subjects.
+    // assertTrue(verifier.verify("bar.com", session));
+    // assertFalse(verifier.verify("a.bar.com", session));
+    // assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
+  }
+
+  @Test public void verifySubjectAltOnly() throws Exception {
+    // subjectAlt=foo.com
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIESjCCAzKgAwIBAgIJAIz+EYMBU6aYMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MjYxMFoXDTI4MTEwNTE2MjYxMFowgZIx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczElMCMGCSqGSIb3DQEJARYWanVsaXVzZGF2aWVzQGdtYWlsLmNv\n"
+        + "bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhjr5aCPoyp0R1iroWA\n"
+        + "fnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2BlYho4O84X244QrZTRl8kQbYt\n"
+        + "xnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRyzerA/ZtrlUqf+lKo0uWcocxe\n"
+        + "Rc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY07hNKXAb2odnVqgzcYiDkLV8\n"
+        + "ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8BqnGd87xQU3FVZI4tbtkB+Kz\n"
+        + "jD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiVJTxpTKqym93whYk93l3ocEe5\n"
+        + "5c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM\n"
+        + "IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86tso4gkJIFiza\n"
+        + "0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0wEgYDVR0RBAsw\n"
+        + "CYIHZm9vLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAjl78oMjzFdsMy6F1sGg/IkO8\n"
+        + "tF5yUgPgFYrs41yzAca7IQu6G9qtFDJz/7ehh/9HoG+oqCCIHPuIOmS7Sd0wnkyJ\n"
+        + "Y7Y04jVXIb3a6f6AgBkEFP1nOT0z6kjT7vkA5LJ2y3MiDcXuRNMSta5PYVnrX8aZ\n"
+        + "yiqVUNi40peuZ2R8mAUSBvWgD7z2qWhF8YgDb7wWaFjg53I36vWKn90ZEti3wNCw\n"
+        + "qAVqixM+J0qJmQStgAc53i2aTMvAQu3A3snvH/PHTBo+5UL72n9S1kZyNCsVf1Qo\n"
+        + "n8jKTiRriEM+fMFlcgQP284EBFzYHyCXFb9O/hMjK2+6mY9euMB1U1aFFzM/Bg==\n"
+        + "-----END CERTIFICATE-----\n");
+    assertTrue(verifier.verify("foo.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    assertTrue(verifier.verify("foo.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+  }
+
+  @Test public void verifyMultipleCn() throws Exception {
+    // CN=foo.com, CN=bar.com, CN=&#x82b1;&#x5b50;.co.jp
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIEbzCCA1egAwIBAgIJAIz+EYMBU6aXMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTk0NVoXDTI4MTEwNTE2MTk0NVowgc0x\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczEQMA4GA1UEAwwHZm9vLmNvbTEQMA4GA1UEAwwHYmFyLmNvbTEV\n"
+        + "MBMGA1UEAwwM6Iqx5a2QLmNvLmpwMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyGOv\n"
+        + "loI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pNjYGViGjg7zhf\n"
+        + "bjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0ZHLN6sD9m2uV\n"
+        + "Sp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1JVjTuE0pcBva\n"
+        + "h2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6q/wGqcZ3zvFB\n"
+        + "TcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYxqJUlPGlMqrKb\n"
+        + "3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQf\n"
+        + "Fh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86\n"
+        + "tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0w\n"
+        + "DQYJKoZIhvcNAQEFBQADggEBAGuZb8ai1NO2j4v3y9TLZvd5s0vh5/TE7n7RX+8U\n"
+        + "y37OL5k7x9nt0mM1TyAKxlCcY+9h6frue8MemZIILSIvMrtzccqNz0V1WKgA+Orf\n"
+        + "uUrabmn+CxHF5gpy6g1Qs2IjVYWA5f7FROn/J+Ad8gJYc1azOWCLQqSyfpNRLSvY\n"
+        + "EriQFEV63XvkJ8JrG62b+2OT2lqT4OO07gSPetppdlSa8NBSKP6Aro9RIX1ZjUZQ\n"
+        + "SpQFCfo02NO0uNRDPUdJx2huycdNb+AXHaO7eXevDLJ+QnqImIzxWiY6zLOdzjjI\n"
+        + "VBMkLHmnP7SjGSQ3XA4ByrQOxfOUTyLyE7NuemhHppuQPxE=\n"
+        + "-----END CERTIFICATE-----\n");
+    assertFalse(verifier.verify("foo.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    assertFalse(verifier.verify("bar.com", session));
+    assertFalse(verifier.verify("a.bar.com", session));
+    assertTrue(verifier.verify("\u82b1\u5b50.co.jp", session));
+    assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
+  }
+
+  @Test public void verifyWilcardCn() throws Exception {
+    // CN=*.foo.com
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIESDCCAzCgAwIBAgIJAIz+EYMBU6aUMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTU1NVoXDTI4MTEwNTE2MTU1NVowgaYx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq\n"
+        + "dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+        + "CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN\n"
+        + "jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0\n"
+        + "ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1\n"
+        + "JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6\n"
+        + "q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx\n"
+        + "qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCG\n"
+        + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+        + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+        + "A1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBAH0ipG6J561UKUfgkeW7GvYwW98B\n"
+        + "N1ZooWX+JEEZK7+Pf/96d3Ij0rw9ACfN4bpfnCq0VUNZVSYB+GthQ2zYuz7tf/UY\n"
+        + "A6nxVgR/IjG69BmsBl92uFO7JTNtHztuiPqBn59pt+vNx4yPvno7zmxsfI7jv0ww\n"
+        + "yfs+0FNm7FwdsC1k47GBSOaGw38kuIVWqXSAbL4EX9GkryGGOKGNh0qvAENCdRSB\n"
+        + "G9Z6tyMbmfRY+dLSh3a9JwoEcBUso6EWYBakLbq4nG/nvYdYvG9ehrnLVwZFL82e\n"
+        + "l3Q/RK95bnA6cuRClGusLad0e6bjkBzx/VQ3VarDEpAkTLUGVAa0CLXtnyc=\n"
+        + "-----END CERTIFICATE-----\n");
+    assertTrue(verifier.verify("foo.com", session));
+    assertTrue(verifier.verify("www.foo.com", session));
+    assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
+    assertFalse(verifier.verify("a.b.foo.com", session));
+  }
+
+  @Test public void verifyWilcardCnOnTld() throws Exception {
+    // It's the CA's responsibility to not issue broad-matching certificates!
+    // CN=*.co.jp
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aVMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTYzMFoXDTI4MTEwNTE2MTYzMFowgaQx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczEQMA4GA1UEAxQHKi5jby5qcDElMCMGCSqGSIb3DQEJARYWanVs\n"
+        + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+        + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+        + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+        + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+        + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+        + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+        + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB\n"
+        + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE\n"
+        + "FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS\n"
+        + "yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQA0sWglVlMx2zNGvUqFC73XtREwii53\n"
+        + "CfMM6mtf2+f3k/d8KXhLNySrg8RRlN11zgmpPaLtbdTLrmG4UdAHHYr8O4y2BBmE\n"
+        + "1cxNfGxxechgF8HX10QV4dkyzp6Z1cfwvCeMrT5G/V1pejago0ayXx+GPLbWlNeZ\n"
+        + "S+Kl0m3p+QplXujtwG5fYcIpaGpiYraBLx3Tadih39QN65CnAh/zRDhLCUzKyt9l\n"
+        + "UGPLEUDzRHMPHLnSqT1n5UU5UDRytbjJPXzF+l/+WZIsanefWLsxnkgAuZe/oMMF\n"
+        + "EJMryEzOjg4Tfuc5qM0EXoPcQ/JlheaxZ40p2IyHqbsWV4MRYuFH4bkM\n"
+        + "-----END CERTIFICATE-----\n");
+    assertTrue(verifier.verify("foo.co.jp", session));
+    assertTrue(verifier.verify("\u82b1\u5b50.co.jp", session));
+  }
+
+  /**
+   * Ignored due to incompatibilities between Android and Java on how non-ASCII
+   * subject alt names are parsed. Android fails to parse these, which means we
+   * fall back to the CN. The RI does parse them, so the CN is unused.
+   */
+  @Test @Ignore public void testWilcardNonAsciiSubjectAlt() throws Exception {
+    // CN=*.foo.com, subjectAlt=*.bar.com, subjectAlt=*.&#x82b1;&#x5b50;.co.jp
+    // (*.hanako.co.jp in kanji)
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIEcDCCA1igAwIBAgIJAIz+EYMBU6aWMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+        + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+        + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+        + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+        + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTczMVoXDTI4MTEwNTE2MTczMVowgaYx\n"
+        + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+        + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+        + "cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq\n"
+        + "dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+        + "CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN\n"
+        + "jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0\n"
+        + "ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1\n"
+        + "JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6\n"
+        + "q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx\n"
+        + "qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo4GiMIGfMAkGA1UdEwQCMAAwLAYJ\n"
+        + "YIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1Ud\n"
+        + "DgQWBBSfFHe/Pzq2yjiCQkgWLNrQy16H2DAfBgNVHSMEGDAWgBR7mtqPkJlOUtKs\n"
+        + "27MDUsqHpg0+DTAkBgNVHREEHTAbggkqLmJhci5jb22CDiou6Iqx5a2QLmNvLmpw\n"
+        + "MA0GCSqGSIb3DQEBBQUAA4IBAQBobWC+D5/lx6YhX64CwZ26XLjxaE0S415ajbBq\n"
+        + "DK7lz+Rg7zOE3GsTAMi+ldUYnhyz0wDiXB8UwKXl0SDToB2Z4GOgqQjAqoMmrP0u\n"
+        + "WB6Y6dpkfd1qDRUzI120zPYgSdsXjHW9q2H77iV238hqIU7qCvEz+lfqqWEY504z\n"
+        + "hYNlknbUnR525ItosEVwXFBJTkZ3Yw8gg02c19yi8TAh5Li3Ad8XQmmSJMWBV4XK\n"
+        + "qFr0AIZKBlg6NZZFf/0dP9zcKhzSriW27bY0XfzA6GSiRDXrDjgXq6baRT6YwgIg\n"
+        + "pgJsDbJtZfHnV1nd3M6zOtQPm1TIQpNmMMMd/DPrGcUQerD3\n"
+        + "-----END CERTIFICATE-----\n");
+    // try the foo.com variations
+    assertTrue(verifier.verify("foo.com", session));
+    assertTrue(verifier.verify("www.foo.com", session));
+    assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
+    assertFalse(verifier.verify("a.b.foo.com", session));
+    // these checks test alternative subjects. The test data contains an
+    // alternative subject starting with a japanese kanji character. This is
+    // not supported by Android because the underlying implementation from
+    // harmony follows the definition from rfc 1034 page 10 for alternative
+    // subject names. This causes the code to drop all alternative subjects.
+    // assertFalse(verifier.verify("bar.com", session));
+    // assertTrue(verifier.verify("www.bar.com", session));
+    // assertTrue(verifier.verify("\u82b1\u5b50.bar.com", session));
+    // assertTrue(verifier.verify("a.b.bar.com", session));
+  }
+
+  @Test public void subjectAltUsesLocalDomainAndIp() throws Exception {
+    // cat cert.cnf
+    // [req]
+    // distinguished_name=distinguished_name
+    // req_extensions=req_extensions
+    // x509_extensions=x509_extensions
+    // [distinguished_name]
+    // [req_extensions]
+    // [x509_extensions]
+    // subjectAltName=DNS:localhost.localdomain,DNS:localhost,IP:127.0.0.1
+    //
+    // $ openssl req -x509 -nodes -days 36500 -subj '/CN=localhost' -config ./cert.cnf \
+    //     -newkey rsa:512 -out cert.pem
+    X509Certificate certificate = certificate(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIBWDCCAQKgAwIBAgIJANS1EtICX2AZMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV\n"
+        + "BAMTCWxvY2FsaG9zdDAgFw0xMjAxMDIxOTA4NThaGA8yMTExMTIwOTE5MDg1OFow\n"
+        + "FDESMBAGA1UEAxMJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPpt\n"
+        + "atK8r4/hf4hSIs0os/BSlQLbRBaK9AfBReM4QdAklcQqe6CHsStKfI8pp0zs7Ptg\n"
+        + "PmMdpbttL0O7mUboBC8CAwEAAaM1MDMwMQYDVR0RBCowKIIVbG9jYWxob3N0Lmxv\n"
+        + "Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEFBQADQQD0ntfL\n"
+        + "DCzOCv9Ma6Lv5o5jcYWVxvBSTsnt22hsJpWD1K7iY9lbkLwl0ivn73pG2evsAn9G\n"
+        + "X8YKH52fnHsCrhSD\n"
+        + "-----END CERTIFICATE-----");
+    assertEquals(new X500Principal("CN=localhost"), certificate.getSubjectX500Principal());
+
+    FakeSSLSession session = new FakeSSLSession(certificate);
+    assertTrue(verifier.verify("localhost", session));
+    assertTrue(verifier.verify("localhost.localdomain", session));
+    assertFalse(verifier.verify("local.host", session));
+
+    assertTrue(verifier.verify("127.0.0.1", session));
+    assertFalse(verifier.verify("127.0.0.2", session));
+  }
+
+  @Test public void wildcardsCannotMatchIpAddresses() throws Exception {
+    // openssl req -x509 -nodes -days 36500 -subj '/CN=*.0.0.1' -newkey rsa:512 -out cert.pem
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIBkjCCATygAwIBAgIJAMdemqOwd/BEMA0GCSqGSIb3DQEBBQUAMBIxEDAOBgNV\n"
+        + "BAMUByouMC4wLjEwIBcNMTAxMjIwMTY0NDI1WhgPMjExMDExMjYxNjQ0MjVaMBIx\n"
+        + "EDAOBgNVBAMUByouMC4wLjEwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAqY8c9Qrt\n"
+        + "YPWCvb7lclI+aDHM6fgbJcHsS9Zg8nUOh5dWrS7AgeA25wyaokFl4plBbbHQe2j+\n"
+        + "cCjsRiJIcQo9HwIDAQABo3MwcTAdBgNVHQ4EFgQUJ436TZPJvwCBKklZZqIvt1Yt\n"
+        + "JjEwQgYDVR0jBDswOYAUJ436TZPJvwCBKklZZqIvt1YtJjGhFqQUMBIxEDAOBgNV\n"
+        + "BAMUByouMC4wLjGCCQDHXpqjsHfwRDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB\n"
+        + "BQUAA0EAk9i88xdjWoewqvE+iMC9tD2obMchgFDaHH0ogxxiRaIKeEly3g0uGxIt\n"
+        + "fl2WRY8hb4x+zRrwsFaLEpdEvqcjOQ==\n"
+        + "-----END CERTIFICATE-----");
+    assertFalse(verifier.verify("127.0.0.1", session));
+  }
+
+  /**
+   * Earlier implementations of Android's hostname verifier required that
+   * wildcard names wouldn't match "*.com" or similar. This was a nonstandard
+   * check that we've since dropped. It is the CA's responsibility to not hand
+   * out certificates that match so broadly.
+   */
+  @Test public void wildcardsDoesNotNeedTwoDots() throws Exception {
+    // openssl req -x509 -nodes -days 36500 -subj '/CN=*.com' -newkey rsa:512 -out cert.pem
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIBjDCCATagAwIBAgIJAOVulXCSu6HuMA0GCSqGSIb3DQEBBQUAMBAxDjAMBgNV\n"
+        + "BAMUBSouY29tMCAXDTEwMTIyMDE2NDkzOFoYDzIxMTAxMTI2MTY0OTM4WjAQMQ4w\n"
+        + "DAYDVQQDFAUqLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDJd8xqni+h7Iaz\n"
+        + "ypItivs9kPuiJUqVz+SuJ1C05SFc3PmlRCvwSIfhyD67fHcbMdl+A/LrIjhhKZJe\n"
+        + "1joO0+pFAgMBAAGjcTBvMB0GA1UdDgQWBBS4Iuzf5w8JdCp+EtBfdFNudf6+YzBA\n"
+        + "BgNVHSMEOTA3gBS4Iuzf5w8JdCp+EtBfdFNudf6+Y6EUpBIwEDEOMAwGA1UEAxQF\n"
+        + "Ki5jb22CCQDlbpVwkruh7jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA0EA\n"
+        + "U6LFxmZr31lFyis2/T68PpjAppc0DpNQuA2m/Y7oTHBDi55Fw6HVHCw3lucuWZ5d\n"
+        + "qUYo4ES548JdpQtcLrW2sA==\n"
+        + "-----END CERTIFICATE-----");
+    assertTrue(verifier.verify("google.com", session));
+  }
+
+  @Test public void subjectAltName() throws Exception {
+    // $ cat ./cert.cnf
+    // [req]
+    // distinguished_name=distinguished_name
+    // req_extensions=req_extensions
+    // x509_extensions=x509_extensions
+    // [distinguished_name]
+    // [req_extensions]
+    // [x509_extensions]
+    // subjectAltName=DNS:bar.com,DNS:baz.com
+    //
+    // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \
+    //     -newkey rsa:512 -out cert.pem
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIBPTCB6KADAgECAgkA7zoHaaqNGHQwDQYJKoZIhvcNAQEFBQAwEjEQMA4GA1UE\n"
+        + "AxMHZm9vLmNvbTAgFw0xMDEyMjAxODM5MzZaGA8yMTEwMTEyNjE4MzkzNlowEjEQ\n"
+        + "MA4GA1UEAxMHZm9vLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC+gmoSxF+8\n"
+        + "hbV+rgRQqHIJd50216OWQJbU3BvdlPbca779NYO4+UZWTFdBM8BdQqs3H4B5Agvp\n"
+        + "y7HeSff1F7XRAgMBAAGjHzAdMBsGA1UdEQQUMBKCB2Jhci5jb22CB2Jhei5jb20w\n"
+        + "DQYJKoZIhvcNAQEFBQADQQBXpZZPOY2Dy1lGG81JTr8L4or9jpKacD7n51eS8iqI\n"
+        + "oTznPNuXHU5bFN0AAGX2ij47f/EahqTpo5RdS95P4sVm\n"
+        + "-----END CERTIFICATE-----");
+    assertFalse(verifier.verify("foo.com", session));
+    assertTrue(verifier.verify("bar.com", session));
+    assertTrue(verifier.verify("baz.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    assertFalse(verifier.verify("quux.com", session));
+  }
+
+  @Test public void subjectAltNameWithWildcard() throws Exception {
+    // $ cat ./cert.cnf
+    // [req]
+    // distinguished_name=distinguished_name
+    // req_extensions=req_extensions
+    // x509_extensions=x509_extensions
+    // [distinguished_name]
+    // [req_extensions]
+    // [x509_extensions]
+    // subjectAltName=DNS:bar.com,DNS:*.baz.com
+    //
+    // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \
+    //     -newkey rsa:512 -out cert.pem
+    SSLSession session = session(""
+        + "-----BEGIN CERTIFICATE-----\n"
+        + "MIIBPzCB6qADAgECAgkAnv/7Jv5r7pMwDQYJKoZIhvcNAQEFBQAwEjEQMA4GA1UE\n"
+        + "AxMHZm9vLmNvbTAgFw0xMDEyMjAxODQ2MDFaGA8yMTEwMTEyNjE4NDYwMVowEjEQ\n"
+        + "MA4GA1UEAxMHZm9vLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDAz2YXnyog\n"
+        + "YdYLSFr/OEgSumtwqtZKJTB4wqTW/eKbBCEzxnyUMxWZIqUGu353PzwfOuWp2re3\n"
+        + "nvVV+QDYQlh9AgMBAAGjITAfMB0GA1UdEQQWMBSCB2Jhci5jb22CCSouYmF6LmNv\n"
+        + "bTANBgkqhkiG9w0BAQUFAANBAB8yrSl8zqy07i0SNYx2B/FnvQY734pxioaqFWfO\n"
+        + "Bqo1ZZl/9aPHEWIwBrxYNVB0SGu/kkbt/vxqOjzzrkXukmI=\n"
+        + "-----END CERTIFICATE-----");
+    assertFalse(verifier.verify("foo.com", session));
+    assertTrue(verifier.verify("bar.com", session));
+    assertTrue(verifier.verify("a.baz.com", session));
+    assertTrue(verifier.verify("baz.com", session));
+    assertFalse(verifier.verify("a.foo.com", session));
+    assertFalse(verifier.verify("a.bar.com", session));
+    assertFalse(verifier.verify("quux.com", session));
+  }
+
+  @Test public void verifyAsIpAddress() {
+    // IPv4
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("127.0.0.1"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("1.2.3.4"));
+
+    // IPv6
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("::1"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("2001:db8::1"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("::192.168.0.1"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("::ffff:192.168.0.1"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("1080:0:0:0:8:800:200C:417A"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("1080::8:800:200C:417A"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("FF01::101"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("0:0:0:0:0:0:13.1.68.3"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("0:0:0:0:0:FFFF:129.144.52.38"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("::13.1.68.3"));
+    assertTrue(OkHostnameVerifier.verifyAsIpAddress("::FFFF:129.144.52.38"));
+
+    // Hostnames
+    assertFalse(OkHostnameVerifier.verifyAsIpAddress("go"));
+    assertFalse(OkHostnameVerifier.verifyAsIpAddress("localhost"));
+    assertFalse(OkHostnameVerifier.verifyAsIpAddress("squareup.com"));
+    assertFalse(OkHostnameVerifier.verifyAsIpAddress("www.nintendo.co.jp"));
+  }
+
+  private X509Certificate certificate(String certificate) throws Exception {
+    return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(
+        new ByteArrayInputStream(certificate.getBytes(Util.UTF_8)));
+  }
+
+  private SSLSession session(String certificate) throws Exception {
+    return new FakeSSLSession(certificate(certificate));
+  }
+}