diff --git a/okhttp/src/main/java/com/squareup/okhttp/Address.java b/okhttp/src/main/java/com/squareup/okhttp/Address.java index a787a94a6..b34bd9128 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Address.java +++ b/okhttp/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; @@ -39,19 +41,22 @@ public final class Address { final SSLSocketFactory sslSocketFactory; final HostnameVerifier hostnameVerifier; final OkAuthenticator authenticator; + final List transports; public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory, - HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy) - throws UnknownHostException { + HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy, + List 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. */ @@ -91,6 +96,14 @@ public final class Address { 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 getTransports() { + return transports; + } + /** * Returns this address's explicitly-specified HTTP proxy, or null to * delegate to the HTTP client's proxy selector. @@ -107,7 +120,8 @@ public final class Address { && this.uriPort == that.uriPort && equal(this.sslSocketFactory, that.sslSocketFactory) && equal(this.hostnameVerifier, that.hostnameVerifier) - && equal(this.authenticator, that.authenticator); + && equal(this.authenticator, that.authenticator) + && equal(this.transports, that.transports); } return false; } @@ -120,6 +134,7 @@ public final class Address { 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/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java index e9ccb9806..ce94a9fa9 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java @@ -134,7 +134,8 @@ public final class Connection implements Closeable { platform.supportTlsIntolerantServer(sslSocket); } - if (route.modernTls) { + boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3"); + if (useNpn) { platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); } @@ -150,8 +151,7 @@ public final class Connection implements Closeable { in = sslSocket.getInputStream(); byte[] selectedProtocol; - if (route.modernTls - && (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(route.address.getUriHost(), true, in, out) diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index 41c65e195..faddbc1f3 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -15,6 +15,7 @@ */ 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; @@ -27,8 +28,10 @@ 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; @@ -36,8 +39,12 @@ import javax.net.ssl.SSLSocketFactory; /** Configures and creates HTTP connections. */ public final class OkHttpClient { + private static final List DEFAULT_TRANSPORTS + = Util.immutableList(Arrays.asList("spdy/3", "http/1.1")); + private Proxy proxy; - private Set failedRoutes = Collections.synchronizedSet(new LinkedHashSet()); + private List transports; + private final Set failedRoutes; private ProxySelector proxySelector; private CookieHandler cookieHandler; private ResponseCache responseCache; @@ -47,6 +54,14 @@ public final class OkHttpClient { private ConnectionPool connectionPool; private boolean followProtocolRedirects = true; + public OkHttpClient() { + this.failedRoutes = Collections.synchronizedSet(new LinkedHashSet()); + } + + 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 @@ -199,6 +214,49 @@ public final class OkHttpClient { 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. + * + *

The following transports are currently supported: + *

+ * + *

This is an evolving set. 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. + * + *

If multiple protocols are specified, NPN will + * be used to negotiate a transport. Future releases may use another mechanism + * (such as ALPN) + * 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 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 getTransports() { + return transports; + } + public HttpURLConnection open(URL url) { String protocol = url.getProtocol(); OkHttpClient copy = copyWithDefaults(); @@ -216,9 +274,8 @@ public final class OkHttpClient { * each field that hasn't been explicitly configured. */ private OkHttpClient copyWithDefaults() { - OkHttpClient result = new OkHttpClient(); + OkHttpClient result = new OkHttpClient(this); result.proxy = proxy; - result.failedRoutes = failedRoutes; result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault(); result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault(); result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault(); @@ -233,6 +290,7 @@ public final class OkHttpClient { : 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/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java index 290e5ea9f..e19ef2178 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java @@ -29,6 +29,9 @@ import java.net.URI; 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.atomic.AtomicReference; /** Junk drawer of utility methods. */ @@ -324,4 +327,9 @@ public final class Util { } return result.toString(); } + + /** Returns an immutable copy of {@code list}. */ + public static List immutableList(List list) { + return Collections.unmodifiableList(new ArrayList(list)); + } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 3a93384f4..34aa9818a 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -278,7 +278,7 @@ public class HttpEngine { hostnameVerifier = policy.hostnameVerifier; } Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory, - hostnameVerifier, policy.authenticator, policy.requestedProxy); + hostnameVerifier, policy.authenticator, policy.requestedProxy, policy.transports); routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT, policy.getFailedRoutes()); } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java index 20633fff0..ee73c7fa3 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -93,6 +93,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */ SSLSocketFactory sslSocketFactory; HostnameVerifier hostnameVerifier; + List transports; OkAuthenticator authenticator; final Set failedRoutes; @@ -115,6 +116,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { this.connectionPool = client.getConnectionPool(); this.sslSocketFactory = client.getSslSocketFactory(); this.hostnameVerifier = client.getHostnameVerifier(); + this.transports = client.getTransports(); this.authenticator = client.getAuthenticator(); this.responseCache = responseCache; } diff --git a/okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java index 28d19f430..a3a9ea87e 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java @@ -72,14 +72,14 @@ public final class ConnectionPoolTest { @Before public void setUp() throws Exception { httpServer.play(); httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null, - HttpAuthenticator.SYSTEM_DEFAULT, 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(), HttpAuthenticator.SYSTEM_DEFAULT, 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()); diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java index 67724ee47..687a39753 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java @@ -80,11 +80,12 @@ public final class RouteSelectorTest { } private final OkAuthenticator authenticator = HttpAuthenticator.SYSTEM_DEFAULT; + private final List 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, authenticator, null); + Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, Collections.EMPTY_SET); @@ -102,7 +103,7 @@ public final class RouteSelectorTest { } @Test public void singleRouteReturnsFailedRoute() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, null); + Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, Collections.EMPTY_SET); @@ -123,7 +124,7 @@ public final class RouteSelectorTest { } @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA); + Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, Collections.EMPTY_SET); @@ -140,7 +141,8 @@ public final class RouteSelectorTest { } @Test public void explicitDirectProxy() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY); + Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY, + transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, Collections.EMPTY_SET); @@ -155,7 +157,7 @@ public final class RouteSelectorTest { } @Test public void proxySelectorReturnsNull() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, null); + Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); proxySelector.proxies = null; RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, @@ -171,7 +173,7 @@ public final class RouteSelectorTest { } @Test public void proxySelectorReturnsNoProxies() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, null); + Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, Collections.EMPTY_SET); @@ -186,7 +188,7 @@ public final class RouteSelectorTest { } @Test public void proxySelectorReturnsMultipleProxies() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, null); + Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); proxySelector.proxies.add(proxyA); proxySelector.proxies.add(proxyB); @@ -220,7 +222,7 @@ public final class RouteSelectorTest { } @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, 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, @@ -237,7 +239,7 @@ public final class RouteSelectorTest { } @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, authenticator, null); + Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); proxySelector.proxies.add(proxyA); proxySelector.proxies.add(proxyB); @@ -277,7 +279,7 @@ public final class RouteSelectorTest { @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, - Proxy.NO_PROXY); + Proxy.NO_PROXY, transports); Set failedRoutes = new LinkedHashSet(); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, failedRoutes); @@ -290,7 +292,7 @@ public final class RouteSelectorTest { @Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, - Proxy.NO_PROXY); + Proxy.NO_PROXY, transports); Set failedRoutes = new LinkedHashSet(); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, failedRoutes); @@ -303,7 +305,7 @@ public final class RouteSelectorTest { @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, - null); + null, transports); proxySelector.proxies.add(proxyA); proxySelector.proxies.add(proxyB); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, @@ -342,7 +344,7 @@ public final class RouteSelectorTest { @Test public void failedRoutesAreLast() throws Exception { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, - Proxy.NO_PROXY); + Proxy.NO_PROXY, transports); Set failedRoutes = new LinkedHashSet(1); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java index 5efd6b242..827648ad3 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java @@ -2476,6 +2476,29 @@ public final class URLConnectionTest { 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();