diff --git a/okhttp3/src/main/java/okhttp3/Cache.java b/okhttp3/src/main/java/okhttp3/Cache.java index 3717417b6..7e0b2431a 100644 --- a/okhttp3/src/main/java/okhttp3/Cache.java +++ b/okhttp3/src/main/java/okhttp3/Cache.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3; import java.io.Closeable; diff --git a/okhttp3/src/main/java/okhttp3/CertificatePinner.java b/okhttp3/src/main/java/okhttp3/CertificatePinner.java index 4c56d6c14..ec5b98c2e 100644 --- a/okhttp3/src/main/java/okhttp3/CertificatePinner.java +++ b/okhttp3/src/main/java/okhttp3/CertificatePinner.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.Set; import javax.net.ssl.SSLPeerUnverifiedException; import okhttp3.internal.Util; +import okhttp3.internal.tls.CertificateChainCleaner; +import okhttp3.internal.tls.TrustRootIndex; import okio.ByteString; import static java.util.Collections.unmodifiableSet; @@ -128,9 +130,11 @@ public final class CertificatePinner { public static final CertificatePinner DEFAULT = new Builder().build(); private final Map> hostnameToPins; + private final TrustRootIndex trustRootIndex; private CertificatePinner(Builder builder) { - hostnameToPins = Util.immutableMap(builder.hostnameToPins); + this.hostnameToPins = Util.immutableMap(builder.hostnameToPins); + this.trustRootIndex = builder.trustRootIndex; } /** @@ -143,6 +147,9 @@ public final class CertificatePinner { */ public void check(String hostname, List peerCertificates) throws SSLPeerUnverifiedException { + if (trustRootIndex != null) { + peerCertificates = new CertificateChainCleaner(trustRootIndex).clean(peerCertificates); + } Set pins = findMatchingPins(hostname); @@ -208,6 +215,10 @@ public final class CertificatePinner { return wildcardPins; } + Builder newBuilder() { + return new Builder(this); + } + /** * Returns the SHA-1 of {@code certificate}'s public key. This uses the mechanism Moxie * Marlinspike describes in Android @@ -227,6 +238,20 @@ public final class CertificatePinner { /** Builds a configured certificate pinner. */ public static final class Builder { private final Map> hostnameToPins = new LinkedHashMap<>(); + private TrustRootIndex trustRootIndex; + + public Builder() { + } + + Builder(CertificatePinner certificatePinner) { + this.hostnameToPins.putAll(certificatePinner.hostnameToPins); + this.trustRootIndex = certificatePinner.trustRootIndex; + } + + public Builder trustRootIndex(TrustRootIndex trustRootIndex) { + this.trustRootIndex = trustRootIndex; + return this; + } /** * Pins certificates for {@code hostname}. diff --git a/okhttp3/src/main/java/okhttp3/ConnectionPool.java b/okhttp3/src/main/java/okhttp3/ConnectionPool.java index db97676d5..701a98813 100644 --- a/okhttp3/src/main/java/okhttp3/ConnectionPool.java +++ b/okhttp3/src/main/java/okhttp3/ConnectionPool.java @@ -117,9 +117,7 @@ public final class ConnectionPool { RealConnection get(Address address, StreamAllocation streamAllocation) { assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { - // TODO(jwilson): this is awkward. We're already holding a lock on 'this', and - // connection.allocationLimit() may also lock the FramedConnection. - if (connection.allocations.size() < connection.allocationLimit() + if (connection.allocations.size() < connection.allocationLimit && address.equals(connection.route().address) && !connection.noNewStreams) { streamAllocation.acquire(connection); diff --git a/okhttp3/src/main/java/okhttp3/Cookie.java b/okhttp3/src/main/java/okhttp3/Cookie.java index 12c4584b1..74e341de4 100644 --- a/okhttp3/src/main/java/okhttp3/Cookie.java +++ b/okhttp3/src/main/java/okhttp3/Cookie.java @@ -559,4 +559,32 @@ public final class Cookie { return result.toString(); } + + @Override public boolean equals(Object other) { + if (!(other instanceof Cookie)) return false; + Cookie that = (Cookie) other; + return that.name.equals(name) + && that.value.equals(value) + && that.domain.equals(domain) + && that.path.equals(path) + && that.expiresAt == expiresAt + && that.secure == secure + && that.httpOnly == httpOnly + && that.persistent == persistent + && that.hostOnly == hostOnly; + } + + @Override public int hashCode() { + int hash = 17; + hash = 31 * hash + name.hashCode(); + hash = 31 * hash + value.hashCode(); + hash = 31 * hash + domain.hashCode(); + hash = 31 * hash + path.hashCode(); + hash = 31 * hash + (int) (expiresAt ^ (expiresAt >>> 32)); + hash = 31 * hash + (secure ? 0 : 1); + hash = 31 * hash + (httpOnly ? 0 : 1); + hash = 31 * hash + (persistent ? 0 : 1); + hash = 31 * hash + (hostOnly ? 0 : 1); + return hash; + } } diff --git a/okhttp3/src/main/java/okhttp3/Handshake.java b/okhttp3/src/main/java/okhttp3/Handshake.java index 022148275..56e1fd4a2 100644 --- a/okhttp3/src/main/java/okhttp3/Handshake.java +++ b/okhttp3/src/main/java/okhttp3/Handshake.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3; import java.security.Principal; diff --git a/okhttp3/src/main/java/okhttp3/OkHttpClient.java b/okhttp3/src/main/java/okhttp3/OkHttpClient.java index 3d7470948..4d5cbce18 100644 --- a/okhttp3/src/main/java/okhttp3/OkHttpClient.java +++ b/okhttp3/src/main/java/okhttp3/OkHttpClient.java @@ -28,13 +28,16 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; import okhttp3.internal.Internal; import okhttp3.internal.InternalCache; +import okhttp3.internal.Platform; import okhttp3.internal.RouteDatabase; import okhttp3.internal.Util; import okhttp3.internal.http.StreamAllocation; import okhttp3.internal.io.RealConnection; import okhttp3.internal.tls.OkHostnameVerifier; +import okhttp3.internal.tls.TrustRootIndex; /** * Factory for {@linkplain Call calls}, which can be used to send HTTP requests and read their @@ -54,7 +57,7 @@ import okhttp3.internal.tls.OkHostnameVerifier; * Response response = clientWith30sTimeout.newCall(request).execute(); * } */ -public final class OkHttpClient implements Cloneable, Call.Factory { +public class OkHttpClient implements Cloneable, Call.Factory { private static final List DEFAULT_PROTOCOLS = Util.immutableList( Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1); @@ -130,6 +133,7 @@ public final class OkHttpClient implements Cloneable, Call.Factory { final InternalCache internalCache; final SocketFactory socketFactory; final SSLSocketFactory sslSocketFactory; + final TrustRootIndex trustRootIndex; final HostnameVerifier hostnameVerifier; final CertificatePinner certificatePinner; final Authenticator proxyAuthenticator; @@ -159,7 +163,13 @@ public final class OkHttpClient implements Cloneable, Call.Factory { this.cache = builder.cache; this.internalCache = builder.internalCache; this.socketFactory = builder.socketFactory; - if (builder.sslSocketFactory != null) { + + boolean isTLS = false; + for (ConnectionSpec spec : connectionSpecs) { + isTLS = isTLS || spec.isTls(); + } + + if (builder.sslSocketFactory != null || !isTLS) { this.sslSocketFactory = builder.sslSocketFactory; } else { try { @@ -170,8 +180,21 @@ public final class OkHttpClient implements Cloneable, Call.Factory { throw new AssertionError(); // The system has no TLS. Just give up. } } + if (sslSocketFactory != null && builder.trustRootIndex == null) { + X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory); + if (trustManager == null) { + throw new IllegalStateException("Unable to extract the trust manager on " + Platform.get() + + ", sslSocketFactory is " + sslSocketFactory.getClass()); + } + this.trustRootIndex = Platform.get().trustRootIndex(trustManager); + this.certificatePinner = builder.certificatePinner.newBuilder() + .trustRootIndex(trustRootIndex) + .build(); + } else { + this.trustRootIndex = builder.trustRootIndex; + this.certificatePinner = builder.certificatePinner; + } this.hostnameVerifier = builder.hostnameVerifier; - this.certificatePinner = builder.certificatePinner; this.proxyAuthenticator = builder.proxyAuthenticator; this.authenticator = builder.authenticator; this.connectionPool = builder.connectionPool; @@ -317,6 +340,7 @@ public final class OkHttpClient implements Cloneable, Call.Factory { InternalCache internalCache; SocketFactory socketFactory; SSLSocketFactory sslSocketFactory; + TrustRootIndex trustRootIndex; HostnameVerifier hostnameVerifier; CertificatePinner certificatePinner; Authenticator proxyAuthenticator; @@ -364,6 +388,7 @@ public final class OkHttpClient implements Cloneable, Call.Factory { this.cache = okHttpClient.cache; this.socketFactory = okHttpClient.socketFactory; this.sslSocketFactory = okHttpClient.sslSocketFactory; + this.trustRootIndex = okHttpClient.trustRootIndex; this.hostnameVerifier = okHttpClient.hostnameVerifier; this.certificatePinner = okHttpClient.certificatePinner; this.proxyAuthenticator = okHttpClient.proxyAuthenticator; @@ -501,6 +526,7 @@ public final class OkHttpClient implements Cloneable, Call.Factory { public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) { if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null"); this.sslSocketFactory = sslSocketFactory; + this.trustRootIndex = null; return this; } diff --git a/okhttp3/src/main/java/okhttp3/internal/ConnectionSpecSelector.java b/okhttp3/src/main/java/okhttp3/internal/ConnectionSpecSelector.java index 3123eece2..1393910b1 100644 --- a/okhttp3/src/main/java/okhttp3/internal/ConnectionSpecSelector.java +++ b/okhttp3/src/main/java/okhttp3/internal/ConnectionSpecSelector.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal; import java.io.IOException; diff --git a/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java b/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java index 34f993a9a..25e12eeb9 100644 --- a/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java +++ b/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal; import java.io.Closeable; diff --git a/okhttp3/src/main/java/okhttp3/internal/FaultHidingSink.java b/okhttp3/src/main/java/okhttp3/internal/FaultHidingSink.java index ef8430c08..e77f6bb44 100644 --- a/okhttp3/src/main/java/okhttp3/internal/FaultHidingSink.java +++ b/okhttp3/src/main/java/okhttp3/internal/FaultHidingSink.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2015 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 okhttp3.internal; import java.io.IOException; diff --git a/okhttp3/src/main/java/okhttp3/internal/NamedRunnable.java b/okhttp3/src/main/java/okhttp3/internal/NamedRunnable.java index 601ab2e72..b5a410360 100644 --- a/okhttp3/src/main/java/okhttp3/internal/NamedRunnable.java +++ b/okhttp3/src/main/java/okhttp3/internal/NamedRunnable.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal; /** diff --git a/okhttp3/src/main/java/okhttp3/internal/Platform.java b/okhttp3/src/main/java/okhttp3/internal/Platform.java index 9e5e5545d..3b370c8b5 100644 --- a/okhttp3/src/main/java/okhttp3/internal/Platform.java +++ b/okhttp3/src/main/java/okhttp3/internal/Platform.java @@ -18,6 +18,7 @@ package okhttp3.internal; import android.util.Log; import java.io.IOException; +import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -29,7 +30,12 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; import okhttp3.Protocol; +import okhttp3.internal.tls.AndroidTrustRootIndex; +import okhttp3.internal.tls.RealTrustRootIndex; +import okhttp3.internal.tls.TrustRootIndex; import okio.Buffer; import static okhttp3.internal.Internal.logger; @@ -55,6 +61,11 @@ import static okhttp3.internal.Internal.logger; * unstable. * * Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library). + * + *

Trust Manager Extraction

+ * + *

Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover the trust + * manager that was used to create an {@link SSLSocketFactory}. */ public class Platform { private static final Platform PLATFORM = findPlatform(); @@ -78,6 +89,14 @@ public class Platform { public void untagSocket(Socket socket) throws SocketException { } + public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { + return null; + } + + public TrustRootIndex trustRootIndex(X509TrustManager trustManager) { + return new RealTrustRootIndex(trustManager.getAcceptedIssuers()); + } + /** * Configure TLS extensions on {@code sslSocket} for {@code route}. * @@ -112,11 +131,13 @@ public class Platform { private static Platform findPlatform() { // Attempt to find Android 2.3+ APIs. try { + Class sslParametersClass; try { - Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl"); + sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl"); } catch (ClassNotFoundException e) { // Older platform before being unbundled. - Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); + sslParametersClass = Class.forName( + "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl"); } OptionalMethod setUseSessionTickets @@ -144,25 +165,34 @@ public class Platform { } catch (ClassNotFoundException | NoSuchMethodException ignored) { } - return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket, - trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols); + return new Android(sslParametersClass, setUseSessionTickets, setHostname, + trafficStatsTagSocket, trafficStatsUntagSocket, getAlpnSelectedProtocol, + setAlpnProtocols); } catch (ClassNotFoundException ignored) { // This isn't an Android runtime. } - // Find Jetty's ALPN extension for OpenJDK. + // Find an Oracle JDK. try { - String negoClassName = "org.eclipse.jetty.alpn.ALPN"; - Class negoClass = Class.forName(negoClassName); - Class providerClass = Class.forName(negoClassName + "$Provider"); - Class clientProviderClass = Class.forName(negoClassName + "$ClientProvider"); - Class serverProviderClass = Class.forName(negoClassName + "$ServerProvider"); - Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass); - Method getMethod = negoClass.getMethod("get", SSLSocket.class); - Method removeMethod = negoClass.getMethod("remove", SSLSocket.class); - return new JdkWithJettyBootPlatform( - putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass); - } catch (ClassNotFoundException | NoSuchMethodException ignored) { + Class sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl"); + + // Find Jetty's ALPN extension for OpenJDK. + try { + String negoClassName = "org.eclipse.jetty.alpn.ALPN"; + Class negoClass = Class.forName(negoClassName); + Class providerClass = Class.forName(negoClassName + "$Provider"); + Class clientProviderClass = Class.forName(negoClassName + "$ClientProvider"); + Class serverProviderClass = Class.forName(negoClassName + "$ServerProvider"); + Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass); + Method getMethod = negoClass.getMethod("get", SSLSocket.class); + Method removeMethod = negoClass.getMethod("remove", SSLSocket.class); + return new JdkWithJettyBootPlatform(sslContextClass, + putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass); + } catch (ClassNotFoundException | NoSuchMethodException ignored) { + } + + return new JdkPlatform(sslContextClass); + } catch (ClassNotFoundException ignored) { } return new Platform(); @@ -172,6 +202,7 @@ public class Platform { private static class Android extends Platform { private static final int MAX_LOG_LENGTH = 4000; + private final Class sslParametersClass; private final OptionalMethod setUseSessionTickets; private final OptionalMethod setHostname; @@ -183,9 +214,11 @@ public class Platform { private final OptionalMethod getAlpnSelectedProtocol; private final OptionalMethod setAlpnProtocols; - public Android(OptionalMethod setUseSessionTickets, OptionalMethod setHostname, - Method trafficStatsTagSocket, Method trafficStatsUntagSocket, - OptionalMethod getAlpnSelectedProtocol, OptionalMethod setAlpnProtocols) { + public Android(Class sslParametersClass, OptionalMethod setUseSessionTickets, + OptionalMethod setHostname, Method trafficStatsTagSocket, + Method trafficStatsUntagSocket, OptionalMethod getAlpnSelectedProtocol, + OptionalMethod setAlpnProtocols) { + this.sslParametersClass = sslParametersClass; this.setUseSessionTickets = setUseSessionTickets; this.setHostname = setHostname; this.trafficStatsTagSocket = trafficStatsTagSocket; @@ -210,6 +243,34 @@ public class Platform { } } + @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { + Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters"); + if (context == null) { + // If that didn't work, try the Google Play Services SSL provider before giving up. This + // must be loaded by the SSLSocketFactory's class loader. + try { + Class gmsSslParametersClass = Class.forName( + "com.google.android.gms.org.conscrypt.SSLParametersImpl", false, + sslSocketFactory.getClass().getClassLoader()); + context = readFieldOrNull(sslSocketFactory, gmsSslParametersClass, "sslParameters"); + } catch (ClassNotFoundException e) { + return null; + } + } + + X509TrustManager x509TrustManager = readFieldOrNull( + context, X509TrustManager.class, "x509TrustManager"); + if (x509TrustManager != null) return x509TrustManager; + + return readFieldOrNull(context, X509TrustManager.class, "trustManager"); + } + + @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) { + TrustRootIndex result = AndroidTrustRootIndex.get(trustManager); + if (result != null) return result; + return super.trustRootIndex(trustManager); + } + @Override public void configureTlsExtensions( SSLSocket sslSocket, String hostname, List protocols) { // Enable SNI and session tickets. @@ -271,18 +332,34 @@ public class Platform { } } + /** JDK 1.7 or better. */ + private static class JdkPlatform extends Platform { + private final Class sslContextClass; + + public JdkPlatform(Class sslContextClass) { + this.sslContextClass = sslContextClass; + } + + @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { + Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context"); + if (context == null) return null; + return readFieldOrNull(context, X509TrustManager.class, "trustManager"); + } + } + /** * OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path. */ - private static class JdkWithJettyBootPlatform extends Platform { + private static class JdkWithJettyBootPlatform extends JdkPlatform { private final Method putMethod; private final Method getMethod; private final Method removeMethod; private final Class clientProviderClass; private final Class serverProviderClass; - public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod, - Class clientProviderClass, Class serverProviderClass) { + public JdkWithJettyBootPlatform(Class sslContextClass, Method putMethod, Method getMethod, + Method removeMethod, Class clientProviderClass, Class serverProviderClass) { + super(sslContextClass); this.putMethod = putMethod; this.getMethod = getMethod; this.removeMethod = removeMethod; @@ -394,4 +471,27 @@ public class Platform { } return result.readByteArray(); } + + static T readFieldOrNull(Object instance, Class fieldType, String fieldName) { + for (Class c = instance.getClass(); c != Object.class; c = c.getSuperclass()) { + try { + Field field = c.getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(instance); + if (value == null || !fieldType.isInstance(value)) return null; + return fieldType.cast(value); + } catch (NoSuchFieldException ignored) { + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + + // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate. + if (!fieldName.equals("delegate")) { + Object delegate = readFieldOrNull(instance, Object.class, "delegate"); + if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName); + } + + return null; + } } diff --git a/okhttp3/src/main/java/okhttp3/internal/Version.java b/okhttp3/src/main/java/okhttp3/internal/Version.java index 3c25ee871..3b86d3a29 100644 --- a/okhttp3/src/main/java/okhttp3/internal/Version.java +++ b/okhttp3/src/main/java/okhttp3/internal/Version.java @@ -17,7 +17,7 @@ package okhttp3.internal; public final class Version { public static String userAgent() { - return "okhttp/3.0.1"; + return "okhttp/3.1.1"; } private Version() { diff --git a/okhttp3/src/main/java/okhttp3/internal/framed/FrameReader.java b/okhttp3/src/main/java/okhttp3/internal/framed/FrameReader.java index cfcfac0be..741a568c0 100644 --- a/okhttp3/src/main/java/okhttp3/internal/framed/FrameReader.java +++ b/okhttp3/src/main/java/okhttp3/internal/framed/FrameReader.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.framed; import java.io.Closeable; diff --git a/okhttp3/src/main/java/okhttp3/internal/framed/FrameWriter.java b/okhttp3/src/main/java/okhttp3/internal/framed/FrameWriter.java index ca1a7e4b8..5e8c7ebef 100644 --- a/okhttp3/src/main/java/okhttp3/internal/framed/FrameWriter.java +++ b/okhttp3/src/main/java/okhttp3/internal/framed/FrameWriter.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.framed; import java.io.Closeable; diff --git a/okhttp3/src/main/java/okhttp3/internal/framed/FramedConnection.java b/okhttp3/src/main/java/okhttp3/internal/framed/FramedConnection.java index 26d511f91..0cfcba72f 100644 --- a/okhttp3/src/main/java/okhttp3/internal/framed/FramedConnection.java +++ b/okhttp3/src/main/java/okhttp3/internal/framed/FramedConnection.java @@ -82,7 +82,7 @@ public final class FramedConnection implements Closeable { */ private final Listener listener; private final Map streams = new HashMap<>(); - private final String hostName; + private final String hostname; private int lastGoodStreamId; private int nextStreamId; private boolean shutdown; @@ -148,14 +148,14 @@ public final class FramedConnection implements Closeable { okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE); } - hostName = builder.hostName; + hostname = builder.hostname; if (protocol == Protocol.HTTP_2) { variant = new Http2(); // Like newSingleThreadExecutor, except lazy creates the thread. pushExecutor = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(), - Util.threadFactory(String.format("OkHttp %s Push Observer", hostName), true)); + Util.threadFactory(String.format("OkHttp %s Push Observer", hostname), true)); // 1 less than SPDY http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-6.9.2 peerSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 65535); peerSettings.set(Settings.MAX_FRAME_SIZE, 0, Http2.INITIAL_MAX_FRAME_SIZE); @@ -342,7 +342,7 @@ public final class FramedConnection implements Closeable { } void writeSynResetLater(final int streamId, final ErrorCode errorCode) { - executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { + executor.submit(new NamedRunnable("OkHttp %s stream %d", hostname, streamId) { @Override public void execute() { try { writeSynReset(streamId, errorCode); @@ -357,7 +357,7 @@ public final class FramedConnection implements Closeable { } void writeWindowUpdateLater(final int streamId, final long unacknowledgedBytesRead) { - executor.execute(new NamedRunnable("OkHttp Window Update %s stream %d", hostName, streamId) { + executor.execute(new NamedRunnable("OkHttp Window Update %s stream %d", hostname, streamId) { @Override public void execute() { try { frameWriter.windowUpdate(streamId, unacknowledgedBytesRead); @@ -390,7 +390,7 @@ public final class FramedConnection implements Closeable { private void writePingLater( final boolean reply, final int payload1, final int payload2, final Ping ping) { executor.execute(new NamedRunnable("OkHttp %s ping %08x%08x", - hostName, payload1, payload2) { + hostname, payload1, payload2) { @Override public void execute() { try { writePing(reply, payload1, payload2, ping); @@ -528,7 +528,7 @@ public final class FramedConnection implements Closeable { public static class Builder { private Socket socket; - private String hostName; + private String hostname; private BufferedSource source; private BufferedSink sink; private Listener listener = Listener.REFUSE_INCOMING_STREAMS; @@ -550,9 +550,9 @@ public final class FramedConnection implements Closeable { } public Builder socket( - Socket socket, String hostName, BufferedSource source, BufferedSink sink) { + Socket socket, String hostname, BufferedSource source, BufferedSink sink) { this.socket = socket; - this.hostName = hostName; + this.hostname = hostname; this.source = source; this.sink = sink; return this; @@ -586,7 +586,7 @@ public final class FramedConnection implements Closeable { final FrameReader frameReader; private Reader(FrameReader frameReader) { - super("OkHttp %s", hostName); + super("OkHttp %s", hostname); this.frameReader = frameReader; } @@ -663,12 +663,12 @@ public final class FramedConnection implements Closeable { inFinished, headerBlock); lastGoodStreamId = streamId; streams.put(streamId, newStream); - executor.execute(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { + executor.execute(new NamedRunnable("OkHttp %s stream %d", hostname, streamId) { @Override public void execute() { try { listener.onStream(newStream); } catch (IOException e) { - logger.log(Level.INFO, "FramedConnection.Listener failure for " + hostName, e); + logger.log(Level.INFO, "FramedConnection.Listener failure for " + hostname, e); try { newStream.close(ErrorCode.PROTOCOL_ERROR); } catch (IOException ignored) { @@ -724,7 +724,7 @@ public final class FramedConnection implements Closeable { streamsToNotify = streams.values().toArray(new FramedStream[streams.size()]); } } - executor.execute(new NamedRunnable("OkHttp %s settings", hostName) { + executor.execute(new NamedRunnable("OkHttp %s settings", hostname) { @Override public void execute() { listener.onSettings(FramedConnection.this); } @@ -740,7 +740,7 @@ public final class FramedConnection implements Closeable { } private void ackSettingsLater(final Settings peerSettings) { - executor.execute(new NamedRunnable("OkHttp %s ACK Settings", hostName) { + executor.execute(new NamedRunnable("OkHttp %s ACK Settings", hostname) { @Override public void execute() { try { frameWriter.ackSettings(peerSettings); @@ -834,7 +834,7 @@ public final class FramedConnection implements Closeable { } currentPushRequests.add(streamId); } - pushExecutor.execute(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) { + pushExecutor.execute(new NamedRunnable("OkHttp %s Push Request[%s]", hostname, streamId) { @Override public void execute() { boolean cancel = pushObserver.onRequest(streamId, requestHeaders); try { @@ -852,7 +852,7 @@ public final class FramedConnection implements Closeable { private void pushHeadersLater(final int streamId, final List

requestHeaders, final boolean inFinished) { - pushExecutor.execute(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) { + pushExecutor.execute(new NamedRunnable("OkHttp %s Push Headers[%s]", hostname, streamId) { @Override public void execute() { boolean cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished); try { @@ -878,7 +878,7 @@ public final class FramedConnection implements Closeable { source.require(byteCount); // Eagerly read the frame before firing client thread. source.read(buffer, byteCount); if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount); - pushExecutor.execute(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) { + pushExecutor.execute(new NamedRunnable("OkHttp %s Push Data[%s]", hostname, streamId) { @Override public void execute() { try { boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished); @@ -895,7 +895,7 @@ public final class FramedConnection implements Closeable { } private void pushResetLater(final int streamId, final ErrorCode errorCode) { - pushExecutor.execute(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) { + pushExecutor.execute(new NamedRunnable("OkHttp %s Push Reset[%s]", hostname, streamId) { @Override public void execute() { pushObserver.onReset(streamId, errorCode); synchronized (FramedConnection.this) { diff --git a/okhttp3/src/main/java/okhttp3/internal/framed/FramedStream.java b/okhttp3/src/main/java/okhttp3/internal/framed/FramedStream.java index 857a99c3c..c94a8449e 100644 --- a/okhttp3/src/main/java/okhttp3/internal/framed/FramedStream.java +++ b/okhttp3/src/main/java/okhttp3/internal/framed/FramedStream.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.framed; import java.io.EOFException; diff --git a/okhttp3/src/main/java/okhttp3/internal/framed/Header.java b/okhttp3/src/main/java/okhttp3/internal/framed/Header.java index 38ac6a686..953671eb3 100644 --- a/okhttp3/src/main/java/okhttp3/internal/framed/Header.java +++ b/okhttp3/src/main/java/okhttp3/internal/framed/Header.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2014 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 okhttp3.internal.framed; import okio.ByteString; diff --git a/okhttp3/src/main/java/okhttp3/internal/framed/Http2.java b/okhttp3/src/main/java/okhttp3/internal/framed/Http2.java index 78ca9ce79..a6c177cc6 100644 --- a/okhttp3/src/main/java/okhttp3/internal/framed/Http2.java +++ b/okhttp3/src/main/java/okhttp3/internal/framed/Http2.java @@ -294,7 +294,7 @@ public final class Http2 implements Variant { case 6: // SETTINGS_MAX_HEADER_LIST_SIZE break; // Advisory only, so ignored. default: - throw ioException("PROTOCOL_ERROR invalid settings id: %s", id); + break; // Must ignore setting with unknown id. } settings.set(id, 0, value); } diff --git a/okhttp3/src/main/java/okhttp3/internal/http/CacheStrategy.java b/okhttp3/src/main/java/okhttp3/internal/http/CacheStrategy.java index a2f513d1d..940bd68b7 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/CacheStrategy.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/CacheStrategy.java @@ -1,3 +1,18 @@ +/* + * 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 okhttp3.internal.http; import java.util.Date; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/HeaderParser.java b/okhttp3/src/main/java/okhttp3/internal/http/HeaderParser.java index 6cb323280..1e27c988c 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/HeaderParser.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/HeaderParser.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; public final class HeaderParser { diff --git a/okhttp3/src/main/java/okhttp3/internal/http/Http1xStream.java b/okhttp3/src/main/java/okhttp3/internal/http/Http1xStream.java index 39ff4d478..320788755 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/Http1xStream.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/Http1xStream.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; import java.io.EOFException; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java b/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java index 2f91dab9b..83cddc3e5 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; import java.io.IOException; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/HttpDate.java b/okhttp3/src/main/java/okhttp3/internal/http/HttpDate.java index b411a17f5..2991dd7b5 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/HttpDate.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/HttpDate.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; import java.text.DateFormat; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java b/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java index 7c463b2de..287bb369e 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; import java.io.IOException; @@ -51,6 +50,7 @@ import okio.Sink; import okio.Source; import okio.Timeout; +import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT; import static java.net.HttpURLConnection.HTTP_MOVED_PERM; import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; import static java.net.HttpURLConnection.HTTP_MULT_CHOICE; @@ -868,8 +868,8 @@ public final class HttpEngine { /** * Figures out the HTTP request to make in response to receiving this engine's response. This will - * either add authentication headers or follow redirects. If a follow-up is either unnecessary or - * not applicable, this returns null. + * either add authentication headers, follow redirects or handle a client request timeout. If a + * follow-up is either unnecessary or not applicable, this returns null. */ public Request followUpRequest() throws IOException { if (userResponse == null) throw new IllegalStateException(); @@ -940,6 +940,17 @@ public final class HttpEngine { return requestBuilder.url(url).build(); + case HTTP_CLIENT_TIMEOUT: + // 408's are rare in practice, but some servers like HAProxy use this response code. The + // spec says that we may repeat the request without modifications. Modern browsers also + // repeat the request (even non-idempotent ones.) + boolean retryableBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink; + if (callerWritesRequestBody && !retryableBody) { + return null; + } + + return userRequest; + default: return null; } diff --git a/okhttp3/src/main/java/okhttp3/internal/http/HttpStream.java b/okhttp3/src/main/java/okhttp3/internal/http/HttpStream.java index d5fc94c7b..34c471345 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/HttpStream.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/HttpStream.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; import java.io.IOException; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/RequestLine.java b/okhttp3/src/main/java/okhttp3/internal/http/RequestLine.java index ff2bc5598..c70a86934 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/RequestLine.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/RequestLine.java @@ -1,3 +1,18 @@ +/* + * 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 okhttp3.internal.http; import java.net.HttpURLConnection; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/RetryableSink.java b/okhttp3/src/main/java/okhttp3/internal/http/RetryableSink.java index 8fb13a8a7..c0010d189 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/RetryableSink.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/RetryableSink.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.http; import java.io.IOException; diff --git a/okhttp3/src/main/java/okhttp3/internal/http/StatusLine.java b/okhttp3/src/main/java/okhttp3/internal/http/StatusLine.java index ccc642b4b..f3591a6bf 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/StatusLine.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/StatusLine.java @@ -1,3 +1,18 @@ +/* + * 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 okhttp3.internal.http; import java.io.IOException; diff --git a/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java b/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java index 3077a7717..e0fe6ccc7 100644 --- a/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java +++ b/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java @@ -44,7 +44,9 @@ import okhttp3.internal.ConnectionSpecSelector; import okhttp3.internal.Platform; import okhttp3.internal.Util; import okhttp3.internal.Version; +import okhttp3.internal.framed.ErrorCode; import okhttp3.internal.framed.FramedConnection; +import okhttp3.internal.framed.FramedStream; import okhttp3.internal.http.Http1xStream; import okhttp3.internal.http.OkHeaders; import okhttp3.internal.http.RouteException; @@ -60,7 +62,7 @@ import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static okhttp3.internal.Util.closeQuietly; -public final class RealConnection implements Connection { +public final class RealConnection extends FramedConnection.Listener implements Connection { private final Route route; /** The low-level TCP socket. */ @@ -77,6 +79,7 @@ public final class RealConnection implements Connection { public int successCount; public BufferedSource source; public BufferedSink sink; + public int allocationLimit; public final List> allocations = new ArrayList<>(); public boolean noNewStreams; public long idleAtNanos = Long.MAX_VALUE; @@ -154,11 +157,15 @@ public final class RealConnection implements Connection { FramedConnection framedConnection = new FramedConnection.Builder(true) .socket(socket, route.address().url().host(), source, sink) .protocol(protocol) + .listener(this) .build(); framedConnection.sendConnectionPreface(); // Only assign the framed connection once the preface has been sent successfully. + this.allocationLimit = framedConnection.maxConcurrentStreams(); this.framedConnection = framedConnection; + } else { + this.allocationLimit = 1; } } @@ -308,13 +315,6 @@ public final class RealConnection implements Connection { return socket; } - public int allocationLimit() { - FramedConnection framedConnection = this.framedConnection; - return framedConnection != null - ? framedConnection.maxConcurrentStreams() - : 1; - } - /** Returns true if this connection is ready to host new streams. */ public boolean isHealthy(boolean doExtensiveChecks) { if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) { @@ -347,6 +347,16 @@ public final class RealConnection implements Connection { return true; } + /** Refuse incoming streams. */ + @Override public void onStream(FramedStream stream) throws IOException { + stream.close(ErrorCode.REFUSED_STREAM); + } + + /** When settings are received, adjust the allocation limit. */ + @Override public void onSettings(FramedConnection connection) { + allocationLimit = connection.maxConcurrentStreams(); + } + @Override public Handshake handshake() { return handshake; } @@ -360,7 +370,11 @@ public final class RealConnection implements Connection { } @Override public Protocol protocol() { - return protocol != null ? protocol : Protocol.HTTP_1_1; + if (framedConnection == null) { + return protocol != null ? protocol : Protocol.HTTP_1_1; + } else { + return framedConnection.getProtocol(); + } } @Override public String toString() { diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java b/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java new file mode 100644 index 000000000..3805939c1 --- /dev/null +++ b/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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 okhttp3.internal.tls; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import javax.net.ssl.X509TrustManager; + +/** + * A index of trusted root certificates that exploits knowledge of Android implementation details. + * This class is potentially much faster to initialize than {@link RealTrustRootIndex} because + * it doesn't need to load and index trusted CA certificates. + */ +public final class AndroidTrustRootIndex implements TrustRootIndex { + private final X509TrustManager trustManager; + private final Method findByIssuerAndSignatureMethod; + + public AndroidTrustRootIndex( + X509TrustManager trustManager, Method findByIssuerAndSignatureMethod) { + this.findByIssuerAndSignatureMethod = findByIssuerAndSignatureMethod; + this.trustManager = trustManager; + } + + @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) { + try { + TrustAnchor trustAnchor = (TrustAnchor) findByIssuerAndSignatureMethod.invoke( + trustManager, cert); + return trustAnchor.getTrustedCert(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + return null; + } + } + + public static TrustRootIndex get(X509TrustManager trustManager) { + // From org.conscrypt.TrustManagerImpl, we want the method with this signature: + // private TrustAnchor findTrustAnchorByIssuerAndSignature(X509Certificate lastCert); + try { + Method method = trustManager.getClass().getDeclaredMethod( + "findTrustAnchorByIssuerAndSignature", X509Certificate.class); + method.setAccessible(true); + return new AndroidTrustRootIndex(trustManager, method); + } catch (NoSuchMethodException e) { + return null; + } + } +} diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java b/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java new file mode 100644 index 000000000..0121aba30 --- /dev/null +++ b/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java @@ -0,0 +1,101 @@ +/* + * 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 okhttp3.internal.tls; + +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import javax.net.ssl.SSLPeerUnverifiedException; + +/** + * Computes the effective certificate chain from the raw array returned by Java's built in TLS APIs. + * Cleaning a chain returns a list of certificates where the first element is {@code chain[0]}, each + * certificate is signed by the certificate that follows, and the last certificate is a trusted CA + * certificate. + * + *

Use of the chain cleaner is necessary to omit unexpected certificates that aren't relevant to + * the TLS handshake and to extract the trusted CA certificate for the benefit of certificate + * pinning. + * + *

This class includes code from Conscrypt's {@code + * TrustManagerImpl} and {@code TrustedCertificateIndex}. + */ +public final class CertificateChainCleaner { + private final TrustRootIndex trustRootIndex; + + public CertificateChainCleaner(TrustRootIndex trustRootIndex) { + this.trustRootIndex = trustRootIndex; + } + + /** + * Returns a cleaned chain for {@code chain}. + * + *

This method throws if the complete chain to a trusted CA certificate cannot be constructed. + * This is unexpected unless the trust root index in this class has a different trust manager than + * what was used to establish {@code chain}. + */ + public List clean(List chain) throws SSLPeerUnverifiedException { + Deque queue = new ArrayDeque<>(chain); + List result = new ArrayList<>(); + result.add(queue.removeFirst()); + + followIssuerChain: + while (true) { + X509Certificate toVerify = (X509Certificate) result.get(result.size() - 1); + + // If this cert has been signed by a trusted CA cert, we're done. Add the trusted CA + // certificate to the end of the chain, unless it's already present. (That would happen if the + // first certificate in the chain is itself a self-signed and trusted CA certificate.) + X509Certificate caCert = trustRootIndex.findByIssuerAndSignature(toVerify); + if (caCert != null) { + if (result.size() > 1 || !toVerify.equals(caCert)) { + result.add(caCert); + } + return result; + } + + // Search for the certificate in the chain that signed this certificate. This is typically the + // next element in the chain, but it could be any element. + for (Iterator i = queue.iterator(); i.hasNext(); ) { + X509Certificate signingCert = (X509Certificate) i.next(); + if (toVerify.getIssuerDN().equals(signingCert.getSubjectDN()) + && verifySignature(toVerify, signingCert)) { + i.remove(); + result.add(signingCert); + continue followIssuerChain; + } + } + + throw new SSLPeerUnverifiedException("Failed to find a cert that signed " + toVerify); + } + } + + /** Returns true if {@code toVerify} was signed by {@code signingCert}'s public key. */ + private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) { + try { + toVerify.verify(signingCert.getPublicKey()); + return true; + } catch (GeneralSecurityException verifyFailed) { + return false; + } + } +} diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/DistinguishedNameParser.java b/okhttp3/src/main/java/okhttp3/internal/tls/DistinguishedNameParser.java index ceecd9c31..616bca534 100644 --- a/okhttp3/src/main/java/okhttp3/internal/tls/DistinguishedNameParser.java +++ b/okhttp3/src/main/java/okhttp3/internal/tls/DistinguishedNameParser.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.tls; import javax.security.auth.x500.X500Principal; diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java b/okhttp3/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java index e6ad9b8c6..a85df784d 100644 --- a/okhttp3/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java +++ b/okhttp3/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package okhttp3.internal.tls; import java.security.cert.Certificate; @@ -57,7 +56,7 @@ public final class OkHostnameVerifier implements HostnameVerifier { public boolean verify(String host, X509Certificate certificate) { return verifyAsIpAddress(host) ? verifyIpAddress(host, certificate) - : verifyHostName(host, certificate); + : verifyHostname(host, certificate); } /** Returns true if {@code certificate} matches {@code ipAddress}. */ @@ -71,14 +70,14 @@ public final class OkHostnameVerifier implements HostnameVerifier { return false; } - /** Returns true if {@code certificate} matches {@code hostName}. */ - private boolean verifyHostName(String hostName, X509Certificate certificate) { - hostName = hostName.toLowerCase(Locale.US); + /** Returns true if {@code certificate} matches {@code hostname}. */ + private boolean verifyHostname(String hostname, X509Certificate certificate) { + hostname = hostname.toLowerCase(Locale.US); boolean hasDns = false; List altNames = getSubjectAltNames(certificate, ALT_DNS_NAME); for (int i = 0, size = altNames.size(); i < size; i++) { hasDns = true; - if (verifyHostName(hostName, altNames.get(i))) { + if (verifyHostname(hostname, altNames.get(i))) { return true; } } @@ -88,7 +87,7 @@ public final class OkHostnameVerifier implements HostnameVerifier { // 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 verifyHostname(hostname, cn); } } @@ -134,17 +133,17 @@ public final class OkHostnameVerifier implements HostnameVerifier { } /** - * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern}. + * Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}. * - * @param hostName lower-case host name. + * @param hostname lower-case host name. * @param pattern domain name pattern from certificate. May be a wildcard pattern such as {@code * *.android.com}. */ - private boolean verifyHostName(String hostName, String pattern) { + private boolean verifyHostname(String hostname, String pattern) { // Basic sanity checks // Check length == 0 instead of .isEmpty() to support Java 5. - if ((hostName == null) || (hostName.length() == 0) || (hostName.startsWith(".")) - || (hostName.endsWith(".."))) { + if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith(".")) + || (hostname.endsWith(".."))) { // Invalid domain name return false; } @@ -154,29 +153,29 @@ public final class OkHostnameVerifier implements HostnameVerifier { return false; } - // Normalize hostName and pattern by turning them into absolute domain names if they are not + // Normalize hostname and pattern by turning them into absolute domain names if they are not // yet absolute. This is needed because server certificates do not normally contain absolute - // names or patterns, but they should be treated as absolute. At the same time, any hostName + // names or patterns, but they should be treated as absolute. At the same time, any hostname // presented to this method should also be treated as absolute for the purposes of matching // to the server certificate. // www.android.com matches www.android.com // www.android.com matches www.android.com. // www.android.com. matches www.android.com. // www.android.com. matches www.android.com - if (!hostName.endsWith(".")) { - hostName += '.'; + if (!hostname.endsWith(".")) { + hostname += '.'; } if (!pattern.endsWith(".")) { pattern += '.'; } - // hostName and pattern are now absolute domain names. + // hostname and pattern are now absolute domain names. pattern = pattern.toLowerCase(Locale.US); - // hostName and pattern are now in lower case -- domain names are case-insensitive. + // hostname and pattern are now in lower case -- domain names are case-insensitive. if (!pattern.contains("*")) { - // Not a wildcard pattern -- hostName and pattern must match exactly. - return hostName.equals(pattern); + // Not a wildcard pattern -- hostname and pattern must match exactly. + return hostname.equals(pattern); } // Wildcard pattern @@ -196,11 +195,11 @@ public final class OkHostnameVerifier implements HostnameVerifier { return false; } - // Optimization: check whether hostName is too short to match the pattern. hostName must be at + // Optimization: check whether hostname is too short to match the pattern. hostName must be at // least as long as the pattern because asterisk must match the whole left-most label and - // hostName starts with a non-empty label. Thus, asterisk has to match one or more characters. - if (hostName.length() < pattern.length()) { - // hostName too short to match the pattern. + // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters. + if (hostname.length() < pattern.length()) { + // hostname too short to match the pattern. return false; } @@ -209,22 +208,22 @@ public final class OkHostnameVerifier implements HostnameVerifier { return false; } - // hostName must end with the region of pattern following the asterisk. + // hostname must end with the region of pattern following the asterisk. String suffix = pattern.substring(1); - if (!hostName.endsWith(suffix)) { - // hostName does not end with the suffix + if (!hostname.endsWith(suffix)) { + // hostname does not end with the suffix return false; } // Check that asterisk did not match across domain name labels. - int suffixStartIndexInHostName = hostName.length() - suffix.length(); - if ((suffixStartIndexInHostName > 0) - && (hostName.lastIndexOf('.', suffixStartIndexInHostName - 1) != -1)) { + int suffixStartIndexInHostname = hostname.length() - suffix.length(); + if ((suffixStartIndexInHostname > 0) + && (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) { // Asterisk is matching across domain name labels -- not permitted. return false; } - // hostName matches pattern + // hostname matches pattern return true; } } diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/RealTrustRootIndex.java b/okhttp3/src/main/java/okhttp3/internal/tls/RealTrustRootIndex.java new file mode 100644 index 000000000..fb688328f --- /dev/null +++ b/okhttp3/src/main/java/okhttp3/internal/tls/RealTrustRootIndex.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 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 okhttp3.internal.tls; + +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.security.auth.x500.X500Principal; + +public final class RealTrustRootIndex implements TrustRootIndex { + private final Map> subjectToCaCerts; + + public RealTrustRootIndex(X509Certificate... caCerts) { + subjectToCaCerts = new LinkedHashMap<>(); + for (X509Certificate caCert : caCerts) { + X500Principal subject = caCert.getSubjectX500Principal(); + List subjectCaCerts = subjectToCaCerts.get(subject); + if (subjectCaCerts == null) { + subjectCaCerts = new ArrayList<>(1); + subjectToCaCerts.put(subject, subjectCaCerts); + } + subjectCaCerts.add(caCert); + } + } + + @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) { + X500Principal issuer = cert.getIssuerX500Principal(); + List subjectCaCerts = subjectToCaCerts.get(issuer); + if (subjectCaCerts == null) return null; + + for (X509Certificate caCert : subjectCaCerts) { + PublicKey publicKey = caCert.getPublicKey(); + try { + cert.verify(publicKey); + return caCert; + } catch (Exception ignored) { + } + } + + return null; + } +} diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/TrustRootIndex.java b/okhttp3/src/main/java/okhttp3/internal/tls/TrustRootIndex.java new file mode 100644 index 000000000..499f120e3 --- /dev/null +++ b/okhttp3/src/main/java/okhttp3/internal/tls/TrustRootIndex.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 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 okhttp3.internal.tls; + +import java.security.cert.X509Certificate; + +public interface TrustRootIndex { + /** Returns the trusted CA certificate that signed {@code cert}. */ + X509Certificate findByIssuerAndSignature(X509Certificate cert); +}