diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealRoutePlanner.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealRoutePlanner.kt index 132d5b3f0f49..1bebefe00910 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealRoutePlanner.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealRoutePlanner.kt @@ -348,6 +348,8 @@ class RealRoutePlanner internal constructor( override fun sameHostAndPort(url: HttpUrl): Boolean { val routeUrl = address.url - return url.port == routeUrl.port && url.host == routeUrl.host + return url.port == routeUrl.port && + url.host == routeUrl.host && + url.scheme == routeUrl.scheme } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RoutePlanner.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RoutePlanner.kt index dff427f2359a..f61717ad573a 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RoutePlanner.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RoutePlanner.kt @@ -69,9 +69,9 @@ interface RoutePlanner { fun hasNext(failedConnection: RealConnection? = null): Boolean /** - * 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. + * Returns true if the host, port, and scheme 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 diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt index e00c97a4e8e1..dd8487d1a3bf 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt @@ -97,6 +97,7 @@ import okhttp3.CallEvent.ResponseFailed import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.RetryDecision +import okhttp3.CallEvent.SecureConnectStart import okhttp3.CertificatePinner.Companion.pin import okhttp3.Credentials.basic import okhttp3.Headers.Companion.headersOf @@ -2208,6 +2209,54 @@ open class CallTest { assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } + @Test + fun redirectFromHttpToHttpsOnSameHostAndPortDoesNotReusePlaintextConnection() { + val httpsUrl = + server + .url("/second") + .newBuilder() + .scheme("https") + .build() + server.enqueue( + MockResponse( + code = 301, + headers = headersOf("Location", httpsUrl.toString()), + ), + ) + server.enqueue(MockResponse(body = "second-response")) + + assertFailsWith { + client.newCall(Request(url = server.url("/first"))).execute() + } + + assertThat(server.takeRequest().requestLine).startsWith("GET /first HTTP/") + assertThat(eventRecorder.recordedEventTypes()).contains(SecureConnectStart::class) + } + + @Test + fun redirectFromHttpsToHttpOnSameHostAndPortDoesNotReuseTlsConnection() { + enableTls() + val httpUrl = + server + .url("/second") + .newBuilder() + .scheme("http") + .build() + server.enqueue( + MockResponse( + code = 301, + headers = headersOf("Location", httpUrl.toString()), + ), + ) + server.enqueue(MockResponse(body = "second-response")) + + assertFailsWith { + client.newCall(Request(url = server.url("/first"))).execute() + } + + assertThat(server.takeRequest().requestLine).startsWith("GET /first HTTP/") + } + @Test fun postRedirectsToGet() { server.enqueue( diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/FakeRoutePlanner.kt b/okhttp/src/jvmTest/kotlin/okhttp3/FakeRoutePlanner.kt index f086e3d49f8b..fa382b7c0857 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/FakeRoutePlanner.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/FakeRoutePlanner.kt @@ -73,7 +73,8 @@ class FakeRoutePlanner( override fun hasNext(failedConnection: RealConnection?): Boolean = deferredPlans.isNotEmpty() || nextPlanIndex < plans.size || autoGeneratePlans - override fun sameHostAndPort(url: HttpUrl): Boolean = url.host == address.url.host && url.port == address.url.port + override fun sameHostAndPort(url: HttpUrl): Boolean = + url.host == address.url.host && url.port == address.url.port && url.scheme == address.url.scheme override fun close() { factory.close()