diff --git a/README.md b/README.md index 105ecbc96e96..22955bffdeab 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,10 @@ Releases Our [change log][changelog] has release history. -The latest release is available on [Maven Central](https://search.maven.org/artifact/com.squareup.okhttp3/okhttp/4.4.0/jar). +The latest release is available on [Maven Central](https://search.maven.org/artifact/com.squareup.okhttp3/okhttp/4.4.1/jar). ```kotlin -implementation("com.squareup.okhttp3:okhttp:4.4.0") +implementation("com.squareup.okhttp3:okhttp:4.4.1") ``` Snapshot builds are [available][snap]. [R8 and ProGuard][r8_proguard] rules are available. @@ -113,10 +113,10 @@ MockWebServer OkHttp includes a library for testing HTTP, HTTPS, and HTTP/2 clients. -The latest release is available on [Maven Central](https://search.maven.org/artifact/com.squareup.okhttp3/mockwebserver/4.4.0/jar). +The latest release is available on [Maven Central](https://search.maven.org/artifact/com.squareup.okhttp3/mockwebserver/4.4.1/jar). ```kotlin -testImplementation("com.squareup.okhttp3:mockwebserver:4.4.0") +testImplementation("com.squareup.okhttp3:mockwebserver:4.4.1") ``` License diff --git a/build.gradle b/build.gradle index 1b7ff7cfdaf1..b541cf5ab417 100644 --- a/build.gradle +++ b/build.gradle @@ -90,7 +90,7 @@ ext.publishedArtifactId = { project -> allprojects { group = 'com.squareup.okhttp3' project.ext.artifactId = rootProject.ext.publishedArtifactId(project) - version = '4.4.0' + version = '4.4.2-SNAPSHOT' repositories { mavenCentral() diff --git a/mockwebserver/README.md b/mockwebserver/README.md index 4cc56a7e8e79..0367e501d45e 100644 --- a/mockwebserver/README.md +++ b/mockwebserver/README.md @@ -142,7 +142,7 @@ server.setDispatcher(dispatcher); ### Download ```kotlin -testImplementation("com.squareup.okhttp3:mockwebserver:4.4.0") +testImplementation("com.squareup.okhttp3:mockwebserver:4.4.1") ``` ### License diff --git a/okhttp-brotli/README.md b/okhttp-brotli/README.md index e45a6670afd0..17d3b7f90c26 100644 --- a/okhttp-brotli/README.md +++ b/okhttp-brotli/README.md @@ -14,7 +14,7 @@ OkHttpClient client = new OkHttpClient.Builder() ``` ```kotlin -implementation("com.squareup.okhttp3:okhttp-brotli:4.4.0") +implementation("com.squareup.okhttp3:okhttp-brotli:4.4.1") ``` [1]: https://github.com/google/brotli diff --git a/okhttp-dnsoverhttps/README.md b/okhttp-dnsoverhttps/README.md index 04365c015938..0fddbd1a7d15 100644 --- a/okhttp-dnsoverhttps/README.md +++ b/okhttp-dnsoverhttps/README.md @@ -7,5 +7,5 @@ API is not considered stable and may change at any time. ### Download ```kotlin -testImplementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.4.0") +testImplementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.4.1") ``` diff --git a/okhttp-logging-interceptor/README.md b/okhttp-logging-interceptor/README.md index 8d234ec89c1f..bff4e200cd1c 100644 --- a/okhttp-logging-interceptor/README.md +++ b/okhttp-logging-interceptor/README.md @@ -37,7 +37,7 @@ Download -------- ```kotlin -implementation("com.squareup.okhttp3:logging-interceptor:4.4.0") +implementation("com.squareup.okhttp3:logging-interceptor:4.4.1") ``` diff --git a/okhttp-sse/README.md b/okhttp-sse/README.md index 8d5c0f45527c..fc746b74f931 100644 --- a/okhttp-sse/README.md +++ b/okhttp-sse/README.md @@ -7,5 +7,5 @@ API is not considered stable and may change at any time. ### Download ```kotlin -testImplementation("com.squareup.okhttp3:okhttp-sse:4.4.0") +testImplementation("com.squareup.okhttp3:okhttp-sse:4.4.1") ``` diff --git a/okhttp-tls/README.md b/okhttp-tls/README.md index 7e806f85f431..add074220a78 100644 --- a/okhttp-tls/README.md +++ b/okhttp-tls/README.md @@ -227,7 +227,7 @@ Download -------- ```kotlin -implementation("com.squareup.okhttp3:okhttp-tls:4.4.0") +implementation("com.squareup.okhttp3:okhttp-tls:4.4.1") ``` [held_certificate]: http://square.github.io/okhttp/4.x/okhttp-tls/okhttp3.tls/-held-certificate/ diff --git a/okhttp-urlconnection/README.md b/okhttp-urlconnection/README.md index 50eeb0c44de9..175c5deb13fa 100644 --- a/okhttp-urlconnection/README.md +++ b/okhttp-urlconnection/README.md @@ -6,5 +6,5 @@ This module integrates OkHttp with `Authenticator` and `CookieHandler` from `jav ### Download ```kotlin -testImplementation("com.squareup.okhttp3:okhttp-urlconnection:4.4.0") +testImplementation("com.squareup.okhttp3:okhttp-urlconnection:4.4.1") ``` diff --git a/okhttp/src/main/java/okhttp3/Handshake.kt b/okhttp/src/main/java/okhttp3/Handshake.kt index 0dd80bdbbe9a..4bc98acc1b69 100644 --- a/okhttp/src/main/java/okhttp3/Handshake.kt +++ b/okhttp/src/main/java/okhttp3/Handshake.kt @@ -48,8 +48,13 @@ class Handshake internal constructor( peerCertificatesFn: () -> List ) { /** Returns a possibly-empty list of certificates that identify the remote peer. */ - @get:JvmName("peerCertificates") val peerCertificates: List by lazy( - peerCertificatesFn) + @get:JvmName("peerCertificates") val peerCertificates: List by lazy { + try { + peerCertificatesFn() + } catch (spue: SSLPeerUnverifiedException) { + listOf() + } + } @JvmName("-deprecated_tlsVersion") @Deprecated( @@ -121,11 +126,7 @@ class Handshake internal constructor( } override fun toString(): String { - val peerCertificatesString = try { - peerCertificates.map { it.name }.toString() - } catch (_: SSLPeerUnverifiedException) { - "Failed: SSLPeerUnverifiedException" - } + val peerCertificatesString = peerCertificates.map { it.name }.toString() return "Handshake{" + "tlsVersion=$tlsVersion " + "cipherSuite=$cipherSuite " + diff --git a/okhttp/src/main/java/okhttp3/internal/connection/Exchange.kt b/okhttp/src/main/java/okhttp3/internal/connection/Exchange.kt index 546738082669..fc44f72be339 100644 --- a/okhttp/src/main/java/okhttp3/internal/connection/Exchange.kt +++ b/okhttp/src/main/java/okhttp3/internal/connection/Exchange.kt @@ -40,7 +40,7 @@ import okio.buffer class Exchange( internal val call: RealCall, internal val eventListener: EventListener, - private val finder: ExchangeFinder, + internal val finder: ExchangeFinder, private val codec: ExchangeCodec ) { /** Returns true if the request body need not complete before the response body starts. */ diff --git a/okhttp/src/main/java/okhttp3/internal/connection/ExchangeFinder.kt b/okhttp/src/main/java/okhttp3/internal/connection/ExchangeFinder.kt index bf71ac2d6eef..2a7bec00a6d9 100644 --- a/okhttp/src/main/java/okhttp3/internal/connection/ExchangeFinder.kt +++ b/okhttp/src/main/java/okhttp3/internal/connection/ExchangeFinder.kt @@ -19,6 +19,7 @@ import java.io.IOException import java.net.Socket import okhttp3.Address import okhttp3.EventListener +import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Route import okhttp3.internal.assertThreadDoesntHoldLock @@ -142,9 +143,10 @@ class ExchangeFinder( synchronized(connectionPool) { if (call.isCanceled()) throw IOException("Canceled") - releasedConnection = call.connection - toClose = if (call.connection != null && - (call.connection!!.noNewExchanges || !call.connection!!.supportsUrl(address.url))) { + val callConnection = call.connection // changes within this overall method + releasedConnection = callConnection + toClose = if (callConnection != null && (callConnection.noNewExchanges || + !sameHostAndPort(callConnection.route().address.url))) { call.releaseConnectionNoEvents() } else { null @@ -332,4 +334,14 @@ class ExchangeFinder( connection.routeFailureCount == 0 && connection.route().address.url.canReuseConnectionFor(address.url) } + + /** + * Returns true if the host and port are unchanged from when this was created. This is used to + * detect if followups need to do a full connection-finding process including DNS resolution, and + * certificate pin checks. + */ + fun sameHostAndPort(url: HttpUrl): Boolean { + val routeUrl = address.url + return url.port == routeUrl.port && url.host == routeUrl.host + } } diff --git a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt index 8ceb9ec9ac44..4dd219bbeeb9 100644 --- a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt +++ b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt @@ -576,9 +576,14 @@ class RealConnection( } // We have a host mismatch. But if the certificate matches, we're still good. - return !noCoalescedConnections && - handshake != null && - OkHostnameVerifier.verify(url.host, handshake!!.peerCertificates[0] as X509Certificate) + return !noCoalescedConnections && handshake != null && certificateSupportHost(url, handshake!!) + } + + private fun certificateSupportHost(url: HttpUrl, handshake: Handshake): Boolean { + val peerCertificates = handshake.peerCertificates + + return peerCertificates.isNotEmpty() && OkHostnameVerifier.verify(url.host, + peerCertificates[0] as X509Certificate) } @Throws(SocketException::class) diff --git a/okhttp/src/main/java/okhttp3/internal/http/RealInterceptorChain.kt b/okhttp/src/main/java/okhttp3/internal/http/RealInterceptorChain.kt index 5062ac820fd8..2cb09a499256 100644 --- a/okhttp/src/main/java/okhttp3/internal/http/RealInterceptorChain.kt +++ b/okhttp/src/main/java/okhttp3/internal/http/RealInterceptorChain.kt @@ -84,7 +84,7 @@ class RealInterceptorChain( calls++ if (exchange != null) { - check(exchange.connection.supportsUrl(request.url)) { + check(exchange.finder.sameHostAndPort(request.url)) { "network interceptor ${interceptors[index - 1]} must retain the same host and port" } check(calls == 1) { diff --git a/okhttp/src/test/java/okhttp3/ConnectionCoalescingTest.java b/okhttp/src/test/java/okhttp3/ConnectionCoalescingTest.java index e0bdfd52371c..1c4e263680cb 100644 --- a/okhttp/src/test/java/okhttp3/ConnectionCoalescingTest.java +++ b/okhttp/src/test/java/okhttp3/ConnectionCoalescingTest.java @@ -19,12 +19,15 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; +import java.security.cert.X509Certificate; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.X509TrustManager; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.testing.PlatformRule; @@ -267,9 +270,25 @@ public final class ConnectionCoalescingTest { } } + @Test public void skipsOnRedirectWhenDnsDontMatch() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location", url.newBuilder().host("differentdns.com").build())); + server.enqueue(new MockResponse() + .setBody("unexpected call")); + + try { + Response response = execute(url); + response.close(); + fail("expected a failed attempt to connect"); + } catch (IOException expected) { + } + } + /** Not in the certificate SAN. */ @Test public void skipsWhenNotSubjectAltName() throws Exception { server.enqueue(new MockResponse()); + server.enqueue(new MockResponse()); assert200Http2Response(execute(url), server.getHostName()); @@ -278,7 +297,21 @@ public final class ConnectionCoalescingTest { try { execute(nonsanUrl); fail("expected a failed attempt to connect"); - } catch (IOException expected) { + } catch (SSLPeerUnverifiedException expected) { + } + } + + @Test public void skipsOnRedirectWhenNotSubjectAltName() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location", url.newBuilder().host("nonsan.com").build())); + server.enqueue(new MockResponse()); + + try { + Response response = execute(url); + response.close(); + fail("expected a failed attempt to connect"); + } catch (SSLPeerUnverifiedException expected) { } } @@ -321,6 +354,24 @@ public final class ConnectionCoalescingTest { } } + @Test public void skipsOnRedirectWhenCertificatePinningFails() throws Exception { + CertificatePinner pinner = new CertificatePinner.Builder() + .add("san.com", "sha1/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") + .build(); + client = client.newBuilder().certificatePinner(pinner).build(); + + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location", url.newBuilder().host("san.com").build())); + server.enqueue(new MockResponse()); + + try { + execute(url); + fail("expected a failed attempt to connect"); + } catch (SSLPeerUnverifiedException expected) { + } + } + /** * Skips coalescing when hostname verifier is overridden since the intention of the hostname * verification is a black box. @@ -341,6 +392,22 @@ public final class ConnectionCoalescingTest { assertThat(client.connectionPool().connectionCount()).isEqualTo(2); } + @Test public void skipsOnRedirectWhenHostnameVerifierUsed() throws Exception { + HostnameVerifier verifier = (name, session) -> true; + client = client.newBuilder().hostnameVerifier(verifier).build(); + + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location", url.newBuilder().host("san.com").build())); + server.enqueue(new MockResponse()); + + assert200Http2Response(execute(url), "san.com"); + + assertThat(client.connectionPool().connectionCount()).isEqualTo(2); + assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); // Fresh connection. + assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); // Fresh connection. + } + /** * Check we would use an existing connection to a later DNS result instead of connecting to the * first DNS result for the first time. @@ -374,7 +441,6 @@ public final class ConnectionCoalescingTest { /** Check that wildcard SANs are supported. */ @Test public void commonThenWildcard() throws Exception { - server.enqueue(new MockResponse()); server.enqueue(new MockResponse()); @@ -432,6 +498,38 @@ public final class ConnectionCoalescingTest { assertThat(client.connectionPool().connectionCount()).isEqualTo(2); } + /** + * Won't coalesce if we can't clean certs e.g. a dev setup. + */ + @Test public void redirectWithDevSetup() throws Exception { + X509TrustManager TRUST_MANAGER = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + client = client.newBuilder().sslSocketFactory(client.sslSocketFactory(), TRUST_MANAGER).build(); + + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse()); + + assert200Http2Response(execute(url), server.getHostName()); + + HttpUrl sanUrl = url.newBuilder().host("san.com").build(); + assert200Http2Response(execute(sanUrl), "san.com"); + + assertThat(client.connectionPool().connectionCount()).isEqualTo(2); + } + private Response execute(HttpUrl url) throws IOException { return client.newCall(new Request.Builder().url(url).build()).execute(); }