From 3511e1a1a82e21b6c0d7f7e813d7d99f967bef71 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 4 Jan 2025 10:46:58 +0000 Subject: [PATCH 1/5] OkHttpClient.Builder network pinning on Android Cleanly pinning network on Android - #8286 Redo the Async DNS API, but internal for now - #8318 --- ...droidAsyncDnsTest.kt => AndroidDnsTest.kt} | 72 +++++--- .../dnsoverhttps/DnsRecordCodecTest.kt | 4 +- okhttp/api/android/okhttp.api | 53 ++---- okhttp/api/jvm/okhttp.api | 27 --- .../{AndroidAsyncDns.kt => AndroidDns.kt} | 56 ++++-- .../okhttp3/android/AndroidSocketFactory.kt | 83 +++++++++ .../okhttp3/android/NetworkSelection.kt | 38 ++++ .../okhttp3/android/internal/AsyncDns.kt | 74 ++++++++ .../android/internal/BlockingAsyncDns.kt | 84 +++++++++ .../android/internal/CombinedAsyncDns.kt | 93 ++++++++++ .../kotlin/okhttp3/android/AndroidDnsTest.kt | 12 +- .../android/AndroidNetworkSelectionTest.kt | 95 ++++++++++ .../okhttp3/android/CombinedAsyncDnsTest.kt | 167 ++++++++++++++++++ .../android/RecordingAsyncDnsCallback.kt | 72 ++++++++ .../okhttp3/android}/ShadowDnsResolver.kt | 2 +- .../kotlin/okhttp3/AsyncDns.kt | 136 -------------- 16 files changed, 822 insertions(+), 246 deletions(-) rename android-test/src/androidTest/java/okhttp/android/test/{AndroidAsyncDnsTest.kt => AndroidDnsTest.kt} (80%) rename okhttp/src/androidMain/kotlin/okhttp3/android/{AndroidAsyncDns.kt => AndroidDns.kt} (59%) create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/internal/AsyncDns.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/internal/BlockingAsyncDns.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/internal/CombinedAsyncDns.kt rename android-test/src/test/kotlin/okhttp/android/test/AndroidAsyncDnsTest.kt => okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidDnsTest.kt (86%) create mode 100644 okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt create mode 100644 okhttp/src/androidUnitTest/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt create mode 100644 okhttp/src/androidUnitTest/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt rename {android-test/src/test/kotlin/okhttp/android/test => okhttp/src/androidUnitTest/kotlin/okhttp3/android}/ShadowDnsResolver.kt (98%) delete mode 100644 okhttp/src/commonJvmAndroid/kotlin/okhttp3/AsyncDns.kt diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidAsyncDnsTest.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidDnsTest.kt similarity index 80% rename from android-test/src/androidTest/java/okhttp/android/test/AndroidAsyncDnsTest.kt rename to android-test/src/androidTest/java/okhttp/android/test/AndroidDnsTest.kt index c144f7912bdb..6ffdf6a307a4 100644 --- a/android-test/src/androidTest/java/okhttp/android/test/AndroidAsyncDnsTest.kt +++ b/android-test/src/androidTest/java/okhttp/android/test/AndroidDnsTest.kt @@ -14,6 +14,8 @@ * limitations under the License. * */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + package okhttp.android.test import android.content.Context @@ -32,11 +34,13 @@ import java.net.UnknownHostException import java.util.concurrent.CountDownLatch import mockwebserver3.MockResponse import mockwebserver3.junit4.MockWebServerRule -import okhttp3.AsyncDns +import okhttp3.Dns import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.android.AndroidAsyncDns +import okhttp3.android.ANDROID +import okhttp3.android.AndroidDns +import okhttp3.android.internal.AsyncDns import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okio.IOException @@ -50,7 +54,7 @@ import org.junit.Test /** * Run with "./gradlew :android-test:connectedCheck -PandroidBuild=true" and make sure ANDROID_SDK_ROOT is set. */ -class AndroidAsyncDnsTest { +class AndroidDnsTest { @JvmField @Rule val serverRule = MockWebServerRule() private lateinit var client: OkHttpClient @@ -74,7 +78,7 @@ class AndroidAsyncDnsTest { client = OkHttpClient.Builder() - .dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6)) + .dns(Dns.ANDROID) .sslSocketFactory(localhost.sslSocketFactory(), localhost.trustManager) .build() @@ -131,25 +135,33 @@ class AndroidAsyncDnsTest { val latch = CountDownLatch(1) // assumes an IPv4 address - AndroidAsyncDns.IPv4.query( - hostname, - object : AsyncDns.Callback { - override fun onResponse( - hostname: String, - addresses: List, - ) { - allAddresses.addAll(addresses) - latch.countDown() - } - - override fun onFailure( - hostname: String, - e: IOException, - ) { - exception = e - latch.countDown() - } - }, + AndroidDns(AndroidDns.DnsClass.IPV4).query( + hostname = hostname, + originatingCall = null, + callback = + object : AsyncDns.Callback { + override fun onAddresses( + hasMore: Boolean, + hostname: String, + addresses: List, + ) { + allAddresses.addAll(addresses) + if (!hasMore) { + latch.countDown() + } + } + + override fun onFailure( + hasMore: Boolean, + hostname: String, + e: IOException, + ) { + exception = e + if (!hasMore) { + latch.countDown() + } + } + }, ) latch.await() @@ -188,7 +200,7 @@ class AndroidAsyncDnsTest { val client = OkHttpClient.Builder() - .dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6)) + .dns(Dns.ANDROID) .socketFactory(network.socketFactory) .build() @@ -200,11 +212,13 @@ class AndroidAsyncDnsTest { } } - private fun assumeNetwork() { - try { - InetAddress.getByName("www.google.com") - } catch (uhe: UnknownHostException) { - throw AssumptionViolatedException(uhe.message, uhe) + companion object { + fun assumeNetwork() { + try { + InetAddress.getByName("www.google.com") + } catch (uhe: UnknownHostException) { + throw AssumptionViolatedException(uhe.message, uhe) + } } } } diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt index 7e29c5da611a..f3ac0fa1ab7d 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt @@ -23,8 +23,8 @@ import assertk.assertions.isEqualTo import java.net.InetAddress import java.net.UnknownHostException import kotlin.test.assertFailsWith -import okhttp3.AsyncDns.Companion.TYPE_A -import okhttp3.AsyncDns.Companion.TYPE_AAAA +import okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_A +import okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_AAAA import okhttp3.dnsoverhttps.DnsRecordCodec.decodeAnswers import okio.ByteString.Companion.decodeHex import org.junit.jupiter.api.Test diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 5dabeaa98ce9..acd791471990 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -27,33 +27,6 @@ public final class okhttp3/Address { public final fun url ()Lokhttp3/HttpUrl; } -public abstract interface class okhttp3/AsyncDns { - public static final field Companion Lokhttp3/AsyncDns$Companion; - public static final field TYPE_A I - public static final field TYPE_AAAA I - public abstract fun query (Ljava/lang/String;Lokhttp3/AsyncDns$Callback;)V -} - -public abstract interface class okhttp3/AsyncDns$Callback { - public abstract fun onFailure (Ljava/lang/String;Ljava/io/IOException;)V - public abstract fun onResponse (Ljava/lang/String;Ljava/util/List;)V -} - -public final class okhttp3/AsyncDns$Companion { - public static final field TYPE_A I - public static final field TYPE_AAAA I - public final fun toDns ([Lokhttp3/AsyncDns;)Lokhttp3/Dns; -} - -public final class okhttp3/AsyncDns$DnsClass : java/lang/Enum { - public static final field IPV4 Lokhttp3/AsyncDns$DnsClass; - public static final field IPV6 Lokhttp3/AsyncDns$DnsClass; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getType ()I - public static fun valueOf (Ljava/lang/String;)Lokhttp3/AsyncDns$DnsClass; - public static fun values ()[Lokhttp3/AsyncDns$DnsClass; -} - public abstract interface class okhttp3/Authenticator { public static final field Companion Lokhttp3/Authenticator$Companion; public static final field JAVA_NET_AUTHENTICATOR Lokhttp3/Authenticator; @@ -1292,15 +1265,25 @@ public abstract class okhttp3/WebSocketListener { public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V } -public final class okhttp3/android/AndroidAsyncDns : okhttp3/AsyncDns { - public static final field Companion Lokhttp3/android/AndroidAsyncDns$Companion; - public fun (Lokhttp3/AsyncDns$DnsClass;Landroid/net/Network;)V - public synthetic fun (Lokhttp3/AsyncDns$DnsClass;Landroid/net/Network;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun query (Ljava/lang/String;Lokhttp3/AsyncDns$Callback;)V +public final class okhttp3/android/AndroidDnsKt { + public static final fun forNetwork (Lokhttp3/Dns$Companion;Landroid/net/Network;)Lokhttp3/Dns; +} + +public final class okhttp3/android/AndroidSocketFactory : javax/net/SocketFactory { + public fun (Landroid/net/Network;)V + public fun createSocket ()Ljava/net/Socket; + public fun createSocket (Ljava/lang/String;I)Ljava/net/Socket; + public fun createSocket (Ljava/lang/String;ILjava/net/InetAddress;I)Ljava/net/Socket; + public fun createSocket (Ljava/net/InetAddress;I)Ljava/net/Socket; + public fun createSocket (Ljava/net/InetAddress;ILjava/net/InetAddress;I)Ljava/net/Socket; + public fun equals (Ljava/lang/Object;)Z + public final fun getNetwork ()Landroid/net/Network; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } -public final class okhttp3/android/AndroidAsyncDns$Companion { - public final fun getIPv4 ()Lokhttp3/android/AndroidAsyncDns; - public final fun getIPv6 ()Lokhttp3/android/AndroidAsyncDns; +public final class okhttp3/android/NetworkSelection { + public static final field INSTANCE Lokhttp3/android/NetworkSelection; + public final fun withNetwork (Lokhttp3/OkHttpClient$Builder;Landroid/net/Network;)Lokhttp3/OkHttpClient$Builder; } diff --git a/okhttp/api/jvm/okhttp.api b/okhttp/api/jvm/okhttp.api index fcc0709091a4..ee3efffdc585 100644 --- a/okhttp/api/jvm/okhttp.api +++ b/okhttp/api/jvm/okhttp.api @@ -27,33 +27,6 @@ public final class okhttp3/Address { public final fun url ()Lokhttp3/HttpUrl; } -public abstract interface class okhttp3/AsyncDns { - public static final field Companion Lokhttp3/AsyncDns$Companion; - public static final field TYPE_A I - public static final field TYPE_AAAA I - public abstract fun query (Ljava/lang/String;Lokhttp3/AsyncDns$Callback;)V -} - -public abstract interface class okhttp3/AsyncDns$Callback { - public abstract fun onFailure (Ljava/lang/String;Ljava/io/IOException;)V - public abstract fun onResponse (Ljava/lang/String;Ljava/util/List;)V -} - -public final class okhttp3/AsyncDns$Companion { - public static final field TYPE_A I - public static final field TYPE_AAAA I - public final fun toDns ([Lokhttp3/AsyncDns;)Lokhttp3/Dns; -} - -public final class okhttp3/AsyncDns$DnsClass : java/lang/Enum { - public static final field IPV4 Lokhttp3/AsyncDns$DnsClass; - public static final field IPV6 Lokhttp3/AsyncDns$DnsClass; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getType ()I - public static fun valueOf (Ljava/lang/String;)Lokhttp3/AsyncDns$DnsClass; - public static fun values ()[Lokhttp3/AsyncDns$DnsClass; -} - public abstract interface class okhttp3/Authenticator { public static final field Companion Lokhttp3/Authenticator$Companion; public static final field JAVA_NET_AUTHENTICATOR Lokhttp3/Authenticator; diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidAsyncDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt similarity index 59% rename from okhttp/src/androidMain/kotlin/okhttp3/android/AndroidAsyncDns.kt rename to okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt index ae7946fff83f..3b59137861c0 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidAsyncDns.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt @@ -24,8 +24,13 @@ import androidx.annotation.RequiresApi import java.net.InetAddress import java.net.UnknownHostException import java.util.concurrent.Executors -import okhttp3.AsyncDns +import okhttp3.Call +import okhttp3.Dns import okhttp3.ExperimentalOkHttpApi +import okhttp3.android.AndroidDns.DnsClass +import okhttp3.android.internal.AsyncDns +import okhttp3.android.internal.BlockingAsyncDns.Companion.asBlocking +import okhttp3.android.internal.CombinedAsyncDns.Companion.union /** * DNS implementation based on android.net.DnsResolver, which submits a request for @@ -37,8 +42,8 @@ import okhttp3.ExperimentalOkHttpApi */ @RequiresApi(Build.VERSION_CODES.Q) @ExperimentalOkHttpApi -class AndroidAsyncDns( - private val dnsClass: AsyncDns.DnsClass, +internal class AndroidDns internal constructor( + private val dnsClass: DnsClass, private val network: Network? = null, ) : AsyncDns { @RequiresApi(Build.VERSION_CODES.Q) @@ -47,6 +52,7 @@ class AndroidAsyncDns( override fun query( hostname: String, + originatingCall: Call?, callback: AsyncDns.Callback, ) { try { @@ -62,15 +68,17 @@ class AndroidAsyncDns( addresses: List, rCode: Int, ) { - callback.onResponse(hostname, addresses) + callback.onAddresses(hasMore = false, hostname = hostname, addresses = addresses) } override fun onError(e: DnsResolver.DnsException) { callback.onFailure( - hostname, - UnknownHostException(e.message).apply { - initCause(e) - }, + hasMore = false, + hostname = hostname, + e = + UnknownHostException(e.message).apply { + initCause(e) + }, ) } }, @@ -79,6 +87,7 @@ class AndroidAsyncDns( // Handle any errors that might leak out // https://issuetracker.google.com/issues/319957694 callback.onFailure( + hasMore = false, hostname, UnknownHostException(e.message).apply { initCause(e) @@ -89,10 +98,37 @@ class AndroidAsyncDns( @ExperimentalOkHttpApi companion object { + internal fun forNetwork(network: Network): AsyncDns { + return union( + AndroidDns(dnsClass = DnsClass.IPV4, network = network), + AndroidDns(dnsClass = DnsClass.IPV6, network = network), + ) + } + @RequiresApi(Build.VERSION_CODES.Q) - val IPv4 = AndroidAsyncDns(dnsClass = AsyncDns.DnsClass.IPV4) + val IPv4 = AndroidDns(dnsClass = DnsClass.IPV4) @RequiresApi(Build.VERSION_CODES.Q) - val IPv6 = AndroidAsyncDns(dnsClass = AsyncDns.DnsClass.IPV6) + val IPv6 = AndroidDns(dnsClass = DnsClass.IPV6) + + internal const val TYPE_A = 1 + internal const val TYPE_AAAA = 28 + } + + /** + * Class of DNS addresses, such that clients that treat these differently, such + * as attempting IPv6 first, can make such decisions. + */ + @ExperimentalOkHttpApi + internal enum class DnsClass(val type: Int) { + IPV4(TYPE_A), + IPV6(TYPE_AAAA), } } + +internal val Dns.Companion.ANDROID: Dns + @RequiresApi(Build.VERSION_CODES.Q) + get() = union(AndroidDns(dnsClass = DnsClass.IPV4), AndroidDns(dnsClass = DnsClass.IPV6)).asBlocking() + +@RequiresApi(Build.VERSION_CODES.Q) +fun Dns.Companion.forNetwork(network: Network): Dns = AndroidDns.forNetwork(network).asBlocking() diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt new file mode 100644 index 000000000000..b8706e7bdbaa --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 Block, 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.android + +import android.net.Network +import android.os.Build +import java.net.InetAddress +import java.net.Socket +import javax.net.SocketFactory +import okhttp3.ExperimentalOkHttpApi + +@ExperimentalOkHttpApi +class AndroidSocketFactory( + val network: Network, +) : SocketFactory() { + private val socketFactory: SocketFactory = network.socketFactory + + override fun createSocket(): Socket { + return socketFactory.createSocket() + } + + override fun createSocket( + host: String?, + port: Int, + ): Socket { + return socketFactory.createSocket(host, port) + } + + override fun createSocket( + host: String?, + port: Int, + localHost: InetAddress?, + localPort: Int, + ): Socket { + return socketFactory.createSocket(host, port, localHost, localPort) + } + + override fun createSocket( + host: InetAddress?, + port: Int, + ): Socket { + return socketFactory.createSocket(host, port) + } + + override fun createSocket( + address: InetAddress?, + port: Int, + localAddress: InetAddress?, + localPort: Int, + ): Socket { + return socketFactory.createSocket(address, port, localAddress, localPort) + } + + override fun hashCode(): Int { + return if (Build.VERSION.SDK_INT >= 23) { + network.networkHandle.hashCode() + } else { + network.toString().hashCode() + } + } + + override fun equals(other: Any?): Boolean { + return other is AndroidSocketFactory && + network == other.network + } + + override fun toString(): String { + return "AndroidSocketFactory{$network}" + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt new file mode 100644 index 000000000000..106c816f1ebd --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Block, 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.android + +import android.net.Network +import android.os.Build +import androidx.annotation.RequiresApi +import javax.net.SocketFactory +import okhttp3.Dns +import okhttp3.ExperimentalOkHttpApi +import okhttp3.OkHttpClient + +@ExperimentalOkHttpApi +object NetworkSelection { + @RequiresApi(Build.VERSION_CODES.Q) + fun OkHttpClient.Builder.withNetwork(network: Network?): OkHttpClient.Builder { + return if (network == null) { + dns(Dns.ANDROID) + .socketFactory(SocketFactory.getDefault()) + } else { + dns(Dns.forNetwork(network)) + .socketFactory(AndroidSocketFactory(network)) + } + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AsyncDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AsyncDns.kt new file mode 100644 index 000000000000..794099321d1c --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AsyncDns.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 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.android.internal + +import java.net.InetAddress +import okhttp3.Call +import okhttp3.ExperimentalOkHttpApi +import okio.IOException + +/** + * An async domain name service that resolves IP addresses for host names. + * + * The main implementations will typically be implemented using specific DNS libraries such as + * * Android DnsResolver + * * OkHttp DnsOverHttps + * * dnsjava Resolver + * + * Implementations of this interface must be safe for concurrent use. + */ +@ExperimentalOkHttpApi +internal interface AsyncDns { + /** + * Query DNS records for `hostname`, in the order they are received. + */ + fun query( + hostname: String, + originatingCall: Call?, + callback: Callback, + ) + + /** + * Callback to receive results from the DNS Queries. + */ + @ExperimentalOkHttpApi + interface Callback { + /** + * Invoked on a successful result from a single lookup step. + * + * @param addresses a non-empty list of addresses + * @param hasMore true if another call to onAddresses or onFailure will be made + */ + fun onAddresses( + hasMore: Boolean, + hostname: String, + addresses: List, + ) + + /** + * Invoked on a failed result from a single lookup step. + * + * @param hasMore true if another call to onAddresses or onFailure will be made + */ + fun onFailure( + hasMore: Boolean, + hostname: String, + e: IOException, + ) + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/internal/BlockingAsyncDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/BlockingAsyncDns.kt new file mode 100644 index 000000000000..80a52036c4ff --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/BlockingAsyncDns.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Block, 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.android.internal + +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.Collections +import java.util.concurrent.CountDownLatch +import okhttp3.Dns +import okio.IOException + +internal class BlockingAsyncDns(val asyncDns: AsyncDns) : Dns { + override fun lookup(hostname: String): List { + val allAddresses = Collections.synchronizedSet(LinkedHashSet()) + val allExceptions = Collections.synchronizedList(mutableListOf()) + + val latch = CountDownLatch(1) + + asyncDns.query( + hostname, + null, + object : AsyncDns.Callback { + override fun onAddresses( + hasMore: Boolean, + hostname: String, + addresses: List, + ) { + allAddresses.addAll(addresses) + + if (!hasMore) { + latch.countDown() + } + } + + override fun onFailure( + hasMore: Boolean, + hostname: String, + e: IOException, + ) { + allExceptions.add(e) + + if (!hasMore) { + latch.countDown() + } + } + }, + ) + + latch.await() + + // No mutations should be possible after this point + if (allAddresses.isEmpty()) { + val first = allExceptions.firstOrNull() ?: UnknownHostException("No results for $hostname") + + allExceptions.drop(1).forEach { + first.addSuppressed(it) + } + + throw first + } + + return allAddresses.toList() + } + + companion object { + /** Returns a [Dns] that blocks until all async results are available. */ + open fun AsyncDns.asBlocking(): Dns { + return BlockingAsyncDns(this) + } + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/internal/CombinedAsyncDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/CombinedAsyncDns.kt new file mode 100644 index 000000000000..cfc3b6798632 --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/CombinedAsyncDns.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Block, 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.android.internal + +import java.net.InetAddress +import java.net.UnknownHostException +import okhttp3.Call +import okio.IOException + +internal class CombinedAsyncDns(val dnsList: List) : AsyncDns { + override fun query( + hostname: String, + originatingCall: Call?, + callback: AsyncDns.Callback, + ) { + var remainingQueries = dnsList.size + val lock = Any() + + if (dnsList.isEmpty()) { + callback.onFailure(false, hostname, UnknownHostException("No configured dns options")) + return + } + + dnsList.forEach { + it.query( + hostname = hostname, + originatingCall = originatingCall, + callback = + object : AsyncDns.Callback { + override fun onAddresses( + hasMore: Boolean, + hostname: String, + addresses: List, + ) { + synchronized(lock) { + if (!hasMore) { + remainingQueries -= 1 + } + + callback.onAddresses(hasMore = remainingQueries > 0, hostname = hostname, addresses = addresses) + } + } + + override fun onFailure( + hasMore: Boolean, + hostname: String, + e: IOException, + ) { + synchronized(lock) { + if (!hasMore) { + remainingQueries -= 1 + } + + callback.onFailure(hasMore = remainingQueries > 0, hostname = hostname, e = e) + } + } + }, + ) + } + } + + companion object { + /** + * Returns an [AsyncDns] that queries all [sources] in parallel, and calls + * the callback for each partial result. + * + * The callback will be passed `hasMore = false` only when all sources + * have no more results. + * + * @param sources one or more AsyncDns sources to query. + */ + fun union(vararg sources: AsyncDns): AsyncDns { + return if (sources.size == 1) { + sources.first() + } else { + CombinedAsyncDns(sources.toList()) + } + } + } +} diff --git a/android-test/src/test/kotlin/okhttp/android/test/AndroidAsyncDnsTest.kt b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidDnsTest.kt similarity index 86% rename from android-test/src/test/kotlin/okhttp/android/test/AndroidAsyncDnsTest.kt rename to okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidDnsTest.kt index da3b0876d62c..b9b4873c20f4 100644 --- a/android-test/src/test/kotlin/okhttp/android/test/AndroidAsyncDnsTest.kt +++ b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidDnsTest.kt @@ -15,14 +15,14 @@ */ @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -package okhttp.android.test +package okhttp3.android import assertk.assertFailure import assertk.assertions.hasMessage import assertk.assertions.isInstanceOf import java.net.UnknownHostException -import okhttp3.AsyncDns -import okhttp3.android.AndroidAsyncDns +import okhttp3.android.AndroidDns.DnsClass +import okhttp3.android.internal.BlockingAsyncDns.Companion.asBlocking import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -31,17 +31,17 @@ import org.robolectric.shadow.api.Shadow @Config(shadows = [ShadowDnsResolver::class], sdk = [34]) @RunWith(RobolectricTestRunner::class) -class AndroidAsyncDnsTest { +class AndroidDnsTest { @Test fun testDnsRequestInvalid() { - val asyncDns = AndroidAsyncDns.IPv4 + val asyncDns = AndroidDns(DnsClass.IPV4) val shadowDns: ShadowDnsResolver = Shadow.extract(asyncDns.resolver) shadowDns.responder = { throw IllegalArgumentException("Network.fromNetworkHandle refusing to instantiate NETID_UNSET Network.") } - val dns = AsyncDns.toDns(asyncDns) + val dns = asyncDns.asBlocking() assertFailure { dns.lookup("google.invalid") diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt new file mode 100644 index 000000000000..dd67f90457a9 --- /dev/null +++ b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 Block, 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. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package okhttp3.android + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotEqualTo +import okhttp3.OkHttpClient +import okhttp3.android.NetworkSelection.withNetwork +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowNetwork + +@Config(shadows = [ShadowDnsResolver::class], sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class AndroidNetworkSelectionTest { + val network1 = ShadowNetwork.newInstance(1) + val network2 = ShadowNetwork.newInstance(2) + + @Test + fun testEquality() { + val defaultClient = + OkHttpClient.Builder() + .withNetwork(network = null) + .build() + + val network1Client = + defaultClient.newBuilder() + .withNetwork(network = network1) + .build() + + assertThat(network1Client.dns).isNotEqualTo(defaultClient.dns) + assertThat(network1Client.socketFactory).isNotEqualTo(defaultClient.socketFactory) + + val network2Client = + network1Client.newBuilder() + .withNetwork(network = network2) + .build() + + assertThat(network2Client.socketFactory).isNotEqualTo(network1Client.socketFactory) + + val defaultClient2 = + network2Client.newBuilder() + .withNetwork(network = null) + .build() + + assertThat(defaultClient2.socketFactory).isNotEqualTo(network2Client.socketFactory) + assertThat(defaultClient2.socketFactory).isEqualTo(defaultClient.socketFactory) + + val network1Client2 = + network2Client.newBuilder() + .withNetwork(network = network1) + .build() + + assertThat(network1Client2.socketFactory).isEqualTo(network1Client.socketFactory) + } + + @Test + fun testNotTaggedOnDefault() { + val defaultClient = + OkHttpClient.Builder() + .withNetwork(network = null) + .build() + + val network1Client = + defaultClient.newBuilder() + .withNetwork(network = network1) + .build() + } + + @Test + fun testTaggedOnSpecific() { + val network1Client = + OkHttpClient.Builder() + .withNetwork(network = network1) + .build() + } +} diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt new file mode 100644 index 000000000000..27c558e638a5 --- /dev/null +++ b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 Block, 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.android + +import assertk.all +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.hasSize +import assertk.assertions.index +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import java.io.IOException +import java.net.InetAddress +import java.net.UnknownHostException +import okhttp3.Call +import okhttp3.android.RecordingAsyncDnsCallback.Event.Addresses +import okhttp3.android.RecordingAsyncDnsCallback.Event.Failure +import okhttp3.android.internal.AsyncDns +import okhttp3.android.internal.CombinedAsyncDns +import org.junit.Test + +class CombinedAsyncDnsTest { + val callback = RecordingAsyncDnsCallback() + val addr1 = InetAddress.getByAddress("google.com", byteArrayOf(127, 0, 0, 1)) + val addr2 = InetAddress.getByAddress("google.com", byteArrayOf(127, 0, 0, 2)) + + @Test + fun empty() { + val dns = CombinedAsyncDns(listOf()) + + dns.query("google.com", null, callback) + + callback.awaitCompletion() + + assertThat(callback.events).all { + hasSize(1) + index(0).isInstanceOf(Failure::class).all { + prop(Failure::hostname).isEqualTo("google.com") + prop(Failure::hasMore).isEqualTo(false) + prop(Failure::e).isInstanceOf(UnknownHostException::class) + } + } + } + + @Test + fun single() { + val dns = CombinedAsyncDns(listOf(FakeAsyncDns(addr1, addr2))) + + dns.query("google.com", null, callback) + + callback.awaitCompletion() + + assertThat(callback.events).all { + hasSize(1) + index(0).isInstanceOf(Addresses::class).all { + prop(Addresses::hostname).isEqualTo("google.com") + prop(Addresses::hasMore).isEqualTo(false) + prop(Addresses::addresses).containsExactly(addr1, addr2) + } + } + } + + @Test + fun double() { + val dns = CombinedAsyncDns(listOf(FakeAsyncDns(addr1), FakeAsyncDns(addr2))) + + dns.query("google.com", null, callback) + + callback.awaitCompletion() + + assertThat(callback.events).all { + hasSize(2) + index(0).isInstanceOf(Addresses::class).all { + prop(Addresses::hostname).isEqualTo("google.com") + prop(Addresses::hasMore).isEqualTo(true) + prop(Addresses::addresses).containsExactly(addr1) + } + index(1).isInstanceOf(Addresses::class).all { + prop(Addresses::hostname).isEqualTo("google.com") + prop(Addresses::hasMore).isEqualTo(false) + prop(Addresses::addresses).containsExactly(addr2) + } + } + } + + @Test + fun failingButSucceeds() { + val e = UnknownHostException("Unknown") + val dns = CombinedAsyncDns(listOf(FakeAsyncDns(addr1), FailingAsyncDns(e))) + + dns.query("google.com", null, callback) + + callback.awaitCompletion() + + assertThat(callback.events).all { + hasSize(2) + index(0).isInstanceOf(Addresses::class).all { + prop(Addresses::hostname).isEqualTo("google.com") + prop(Addresses::hasMore).isEqualTo(true) + prop(Addresses::addresses).containsExactly(addr1) + } + index(1).isInstanceOf(Failure::class).all { + prop(Failure::hostname).isEqualTo("google.com") + prop(Failure::hasMore).isEqualTo(false) + prop(Failure::e).isInstanceOf(UnknownHostException::class) + } + } + } + + @Test + fun failingAndFails() { + val e = UnknownHostException("Unknown") + val dns = CombinedAsyncDns(listOf(FailingAsyncDns(e), FailingAsyncDns(e))) + + dns.query("google.com", null, callback) + + callback.awaitCompletion() + + assertThat(callback.events).all { + hasSize(2) + index(0).isInstanceOf(Failure::class).all { + prop(Failure::hostname).isEqualTo("google.com") + prop(Failure::hasMore).isEqualTo(true) + prop(Failure::e).isInstanceOf(UnknownHostException::class) + } + index(1).isInstanceOf(Failure::class).all { + prop(Failure::hostname).isEqualTo("google.com") + prop(Failure::hasMore).isEqualTo(false) + prop(Failure::e).isInstanceOf(UnknownHostException::class) + } + } + } + + private class FakeAsyncDns(vararg val addresses: InetAddress) : AsyncDns { + override fun query( + hostname: String, + originatingCall: Call?, + callback: AsyncDns.Callback, + ) { + callback.onAddresses(false, hostname, addresses.toList()) + } + } + + private class FailingAsyncDns(val e: IOException) : AsyncDns { + override fun query( + hostname: String, + originatingCall: Call?, + callback: AsyncDns.Callback, + ) { + callback.onFailure(false, hostname, e) + } + } +} diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt new file mode 100644 index 000000000000..cd10e2e19e1e --- /dev/null +++ b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 Block, 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.android + +import java.net.InetAddress +import java.util.Collections +import okhttp3.android.internal.AsyncDns +import okio.IOException + +class RecordingAsyncDnsCallback : AsyncDns.Callback { + val events = Collections.synchronizedList(mutableListOf()) + + override fun onAddresses( + hasMore: Boolean, + hostname: String, + addresses: List, + ) { + add(Event.Addresses(hasMore, hostname, addresses)) + } + + override fun onFailure( + hasMore: Boolean, + hostname: String, + e: IOException, + ) { + add(Event.Failure(hasMore, hostname, e)) + } + + private fun add(event: Event) { + synchronized(events) { + events.add(event) + (events as Object).notifyAll() + } + } + + fun awaitCompletion() { + synchronized(events) { + while (events.find { !it.hasMore } == null) { + (events as Object).wait() + } + } + } + + sealed interface Event { + val hasMore: Boolean + + data class Addresses( + override val hasMore: Boolean, + val hostname: String, + val addresses: List, + ) : Event + + data class Failure( + override val hasMore: Boolean, + val hostname: String, + val e: IOException, + ) : Event + } +} diff --git a/android-test/src/test/kotlin/okhttp/android/test/ShadowDnsResolver.kt b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/ShadowDnsResolver.kt similarity index 98% rename from android-test/src/test/kotlin/okhttp/android/test/ShadowDnsResolver.kt rename to okhttp/src/androidUnitTest/kotlin/okhttp3/android/ShadowDnsResolver.kt index cf8b84f04df4..6d5eac39afce 100644 --- a/android-test/src/test/kotlin/okhttp/android/test/ShadowDnsResolver.kt +++ b/okhttp/src/androidUnitTest/kotlin/okhttp3/android/ShadowDnsResolver.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp.android.test +package okhttp3.android import android.net.DnsResolver import android.net.Network diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/AsyncDns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/AsyncDns.kt deleted file mode 100644 index fbefc55fb0c0..000000000000 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/AsyncDns.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2022 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 - -import java.net.InetAddress -import java.net.UnknownHostException -import java.util.concurrent.CountDownLatch -import okio.IOException - -/** - * An async domain name service that resolves IP addresses for host names. - * - * The main implementations will typically be implemented using specific DNS libraries such as - * * Android DnsResolver - * * OkHttp DnsOverHttps - * * dnsjava Resolver - * - * Implementations of this interface must be safe for concurrent use. - */ -@ExperimentalOkHttpApi -interface AsyncDns { - /** - * Query DNS records for `hostname`, in the order they are received. - */ - fun query( - hostname: String, - callback: Callback, - ) - - /** - * Callback to receive results from the DNS Queries. - */ - @ExperimentalOkHttpApi - interface Callback { - /** - * Return addresses for a dns query for a single class of IPv4 (A) or IPv6 (AAAA). - * May be an empty list indicating that the host is unreachable. - */ - fun onResponse( - hostname: String, - addresses: List, - ) - - /** - * Returns an error for the DNS query. - */ - fun onFailure( - hostname: String, - e: IOException, - ) - } - - /** - * Class of DNS addresses, such that clients that treat these differently, such - * as attempting IPv6 first, can make such decisions. - */ - @ExperimentalOkHttpApi - enum class DnsClass(val type: Int) { - IPV4(TYPE_A), - IPV6(TYPE_AAAA), - } - - @ExperimentalOkHttpApi - companion object { - const val TYPE_A = 1 - const val TYPE_AAAA = 28 - - /** - * Adapt an AsyncDns implementation to Dns, waiting until onComplete is received - * and returning results if available. - */ - fun toDns(vararg asyncDns: AsyncDns): Dns = - Dns { hostname -> - val allAddresses = mutableListOf() - val allExceptions = mutableListOf() - val latch = CountDownLatch(asyncDns.size) - - asyncDns.forEach { - it.query( - hostname, - object : Callback { - override fun onResponse( - hostname: String, - addresses: List, - ) { - synchronized(allAddresses) { - allAddresses.addAll(addresses) - } - latch.countDown() - } - - override fun onFailure( - hostname: String, - e: IOException, - ) { - synchronized(allExceptions) { - allExceptions.add(e) - } - latch.countDown() - } - }, - ) - } - - latch.await() - - // No mutations should be possible after this point - if (allAddresses.isEmpty()) { - val first = allExceptions.firstOrNull() ?: UnknownHostException("No results for $hostname") - - allExceptions.drop(1).forEach { - first.addSuppressed(it) - } - - throw first - } - - allAddresses - } - } -} From 123a0e635d459a0786f16ffa83323cf624563426 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 4 Jan 2025 10:56:09 +0000 Subject: [PATCH 2/5] Fix robolectric tests --- android-test/build.gradle.kts | 10 +++++----- .../src/test}/kotlin/okhttp3/android/AndroidDnsTest.kt | 0 .../okhttp3/android/AndroidNetworkSelectionTest.kt | 0 .../kotlin/okhttp3/android/CombinedAsyncDnsTest.kt | 0 .../okhttp3/android/RecordingAsyncDnsCallback.kt | 0 .../test}/kotlin/okhttp3/android/ShadowDnsResolver.kt | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename {okhttp/src/androidUnitTest => android-test/src/test}/kotlin/okhttp3/android/AndroidDnsTest.kt (100%) rename {okhttp/src/androidUnitTest => android-test/src/test}/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt (100%) rename {okhttp/src/androidUnitTest => android-test/src/test}/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt (100%) rename {okhttp/src/androidUnitTest => android-test/src/test}/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt (100%) rename {okhttp/src/androidUnitTest => android-test/src/test}/kotlin/okhttp3/android/ShadowDnsResolver.kt (100%) diff --git a/android-test/build.gradle.kts b/android-test/build.gradle.kts index 1dc021fd47b2..59918c44bc24 100644 --- a/android-test/build.gradle.kts +++ b/android-test/build.gradle.kts @@ -9,7 +9,7 @@ plugins { val androidBuild = property("androidBuild").toString().toBoolean() android { - compileSdk = 34 + compileSdk = 35 namespace = "okhttp.android.test" @@ -35,16 +35,16 @@ android { } compileOptions { - targetCompatibility(JavaVersion.VERSION_11) - sourceCompatibility(JavaVersion.VERSION_11) + targetCompatibility(JavaVersion.VERSION_17) + sourceCompatibility(JavaVersion.VERSION_17) } testOptions { - targetSdk = 34 + targetSdk = 35 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } // issue merging due to conflict with httpclient and something else diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidDnsTest.kt b/android-test/src/test/kotlin/okhttp3/android/AndroidDnsTest.kt similarity index 100% rename from okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidDnsTest.kt rename to android-test/src/test/kotlin/okhttp3/android/AndroidDnsTest.kt diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt b/android-test/src/test/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt similarity index 100% rename from okhttp/src/androidUnitTest/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt rename to android-test/src/test/kotlin/okhttp3/android/AndroidNetworkSelectionTest.kt diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt b/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt similarity index 100% rename from okhttp/src/androidUnitTest/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt rename to android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt b/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt similarity index 100% rename from okhttp/src/androidUnitTest/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt rename to android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt diff --git a/okhttp/src/androidUnitTest/kotlin/okhttp3/android/ShadowDnsResolver.kt b/android-test/src/test/kotlin/okhttp3/android/ShadowDnsResolver.kt similarity index 100% rename from okhttp/src/androidUnitTest/kotlin/okhttp3/android/ShadowDnsResolver.kt rename to android-test/src/test/kotlin/okhttp3/android/ShadowDnsResolver.kt From 5ea6fd034f33eff95cb09fae9419f601eda2c6d9 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 4 Jan 2025 10:57:23 +0000 Subject: [PATCH 3/5] Fix robolectric tests --- .../src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt | 1 + .../src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt b/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt index 27c558e638a5..ad4b57e54edf 100644 --- a/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt +++ b/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package okhttp3.android import assertk.all diff --git a/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt b/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt index cd10e2e19e1e..ce107419788d 100644 --- a/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt +++ b/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package okhttp3.android import java.net.InetAddress From e3ea3a88d2b5da98f12586bb36bd5994b1dccaec Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 4 Jan 2025 11:02:15 +0000 Subject: [PATCH 4/5] Improve android api visibility --- okhttp/api/android/okhttp.api | 14 +--------- .../kotlin/okhttp3/android/AndroidDns.kt | 8 +++++- .../okhttp3/android/NetworkSelection.kt | 1 + .../{ => internal}/AndroidSocketFactory.kt | 28 +++++++++---------- 4 files changed, 23 insertions(+), 28 deletions(-) rename okhttp/src/androidMain/kotlin/okhttp3/android/{ => internal}/AndroidSocketFactory.kt (84%) diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index acd791471990..0b186d36895e 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -1267,19 +1267,7 @@ public abstract class okhttp3/WebSocketListener { public final class okhttp3/android/AndroidDnsKt { public static final fun forNetwork (Lokhttp3/Dns$Companion;Landroid/net/Network;)Lokhttp3/Dns; -} - -public final class okhttp3/android/AndroidSocketFactory : javax/net/SocketFactory { - public fun (Landroid/net/Network;)V - public fun createSocket ()Ljava/net/Socket; - public fun createSocket (Ljava/lang/String;I)Ljava/net/Socket; - public fun createSocket (Ljava/lang/String;ILjava/net/InetAddress;I)Ljava/net/Socket; - public fun createSocket (Ljava/net/InetAddress;I)Ljava/net/Socket; - public fun createSocket (Ljava/net/InetAddress;ILjava/net/InetAddress;I)Ljava/net/Socket; - public fun equals (Ljava/lang/Object;)Z - public final fun getNetwork ()Landroid/net/Network; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public static final fun getANDROID (Lokhttp3/Dns$Companion;)Lokhttp3/Dns; } public final class okhttp3/android/NetworkSelection { diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt index 3b59137861c0..d3e093d0152a 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt @@ -126,9 +126,15 @@ internal class AndroidDns internal constructor( } } -internal val Dns.Companion.ANDROID: Dns +/** + * Dns implementation based on [android.net.DnsResolver] instead of [InetAddress.getAllByName]. + */ +val Dns.Companion.ANDROID: Dns @RequiresApi(Build.VERSION_CODES.Q) get() = union(AndroidDns(dnsClass = DnsClass.IPV4), AndroidDns(dnsClass = DnsClass.IPV6)).asBlocking() +/** + * Returns a Dns implementation for a specific Android network, using [android.net.DnsResolver]. + */ @RequiresApi(Build.VERSION_CODES.Q) fun Dns.Companion.forNetwork(network: Network): Dns = AndroidDns.forNetwork(network).asBlocking() diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt index 106c816f1ebd..97bf00d16840 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/NetworkSelection.kt @@ -22,6 +22,7 @@ import javax.net.SocketFactory import okhttp3.Dns import okhttp3.ExperimentalOkHttpApi import okhttp3.OkHttpClient +import okhttp3.android.internal.AndroidSocketFactory @ExperimentalOkHttpApi object NetworkSelection { diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt similarity index 84% rename from okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt rename to okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt index b8706e7bdbaa..e1d0e9de9699 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidSocketFactory.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.android +package okhttp3.android.internal import android.net.Network import android.os.Build +import okhttp3.ExperimentalOkHttpApi import java.net.InetAddress import java.net.Socket import javax.net.SocketFactory -import okhttp3.ExperimentalOkHttpApi @ExperimentalOkHttpApi -class AndroidSocketFactory( - val network: Network, +internal class AndroidSocketFactory( + val network: Network, ) : SocketFactory() { private val socketFactory: SocketFactory = network.socketFactory @@ -40,26 +40,26 @@ class AndroidSocketFactory( } override fun createSocket( - host: String?, - port: Int, - localHost: InetAddress?, - localPort: Int, + host: String?, + port: Int, + localHost: InetAddress?, + localPort: Int, ): Socket { return socketFactory.createSocket(host, port, localHost, localPort) } override fun createSocket( - host: InetAddress?, - port: Int, + host: InetAddress?, + port: Int, ): Socket { return socketFactory.createSocket(host, port) } override fun createSocket( - address: InetAddress?, - port: Int, - localAddress: InetAddress?, - localPort: Int, + address: InetAddress?, + port: Int, + localAddress: InetAddress?, + localPort: Int, ): Socket { return socketFactory.createSocket(address, port, localAddress, localPort) } From 6995217214486be1c256938e5b0be12c4ad21943 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 4 Jan 2025 12:29:25 +0000 Subject: [PATCH 5/5] reformat --- .../okhttp3/android/CombinedAsyncDnsTest.kt | 1 + .../android/RecordingAsyncDnsCallback.kt | 1 + .../android/internal/AndroidSocketFactory.kt | 24 +++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt b/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt index ad4b57e54edf..e6c37f2ac37a 100644 --- a/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt +++ b/android-test/src/test/kotlin/okhttp3/android/CombinedAsyncDnsTest.kt @@ -14,6 +14,7 @@ * limitations under the License. */ @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + package okhttp3.android import assertk.all diff --git a/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt b/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt index ce107419788d..8b88a9cb1729 100644 --- a/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt +++ b/android-test/src/test/kotlin/okhttp3/android/RecordingAsyncDnsCallback.kt @@ -14,6 +14,7 @@ * limitations under the License. */ @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + package okhttp3.android import java.net.InetAddress diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt index e1d0e9de9699..1e5d48db6786 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidSocketFactory.kt @@ -17,14 +17,14 @@ package okhttp3.android.internal import android.net.Network import android.os.Build -import okhttp3.ExperimentalOkHttpApi import java.net.InetAddress import java.net.Socket import javax.net.SocketFactory +import okhttp3.ExperimentalOkHttpApi @ExperimentalOkHttpApi internal class AndroidSocketFactory( - val network: Network, + val network: Network, ) : SocketFactory() { private val socketFactory: SocketFactory = network.socketFactory @@ -40,26 +40,26 @@ internal class AndroidSocketFactory( } override fun createSocket( - host: String?, - port: Int, - localHost: InetAddress?, - localPort: Int, + host: String?, + port: Int, + localHost: InetAddress?, + localPort: Int, ): Socket { return socketFactory.createSocket(host, port, localHost, localPort) } override fun createSocket( - host: InetAddress?, - port: Int, + host: InetAddress?, + port: Int, ): Socket { return socketFactory.createSocket(host, port) } override fun createSocket( - address: InetAddress?, - port: Int, - localAddress: InetAddress?, - localPort: Int, + address: InetAddress?, + port: Int, + localAddress: InetAddress?, + localPort: Int, ): Socket { return socketFactory.createSocket(address, port, localAddress, localPort) }