diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinning.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinning.kt deleted file mode 100644 index 245f9d99e156..000000000000 --- a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinning.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2026 OkHttp Authors - * - * 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 okhttp.android.test - -import android.annotation.SuppressLint -import android.net.Network -import android.os.Build -import java.net.InetAddress -import okhttp3.Dns -import okhttp3.Interceptor -import okhttp3.Response -import okhttp3.android.EchAwareDns - -/** - * Interceptor that supports Network Pinning on Android via Request tags. - * - * Apply as an Application [Interceptor] and based on [okhttp3.Request.tag] with type [Network], - * the appropriate [okhttp3.Dns] and [javax.net.SocketFactory] will be configured. - * - * Copied from https://github.com/square/okhttp/pull/8376, which proposed this as library code. - */ -@SuppressLint("NewApi") -class AndroidNetworkPinning : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - val pinnedNetwork = request.tag() - - val effectiveChain = - if (pinnedNetwork != null) { - chain - .withSocketFactory(pinnedNetwork.socketFactory) - .withDns(dnsForNetwork(pinnedNetwork)) - } else { - chain - } - - return effectiveChain.proceed(request) - } - - /** - * ECH needs the `HTTPS` record, which is only reachable through `DnsResolver.rawQuery()` and only - * consulted by the platform from API 37. Below that there's nothing to gain from the extra query, - * so [AndroidNetworkDns] does the plain address lookup. - */ - private fun dnsForNetwork(network: Network): Dns = - when { - Build.VERSION.SDK_INT >= 37 -> EchAwareDns.forNetwork(network) - else -> AndroidNetworkDns(network) - } -} - -/** - * A [Dns] scoped to [network], used below API 37 where there's no ECH to resolve for. - * - * [Network.getAllByName] is the whole implementation: it resolves on that network and nothing else, - * with no service metadata. - */ -class AndroidNetworkDns( - private val network: Network, -) : Dns { - override fun lookup(hostname: String): List = network.getAllByName(hostname).toList() -} diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworksTest.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworksTest.kt deleted file mode 100644 index 68682fd1d7a7..000000000000 --- a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworksTest.kt +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (c) 2026 OkHttp Authors - * - * 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 okhttp.android.test - -import android.content.Context -import android.net.ConnectivityManager -import android.net.ConnectivityManager.NetworkCallback -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET -import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED -import android.net.NetworkCapabilities.TRANSPORT_CELLULAR -import android.net.NetworkCapabilities.TRANSPORT_WIFI -import android.net.NetworkRequest -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import assertk.assertThat -import assertk.assertions.isEqualTo -import assertk.assertions.isIn -import assertk.assertions.isNotEqualTo -import assertk.assertions.isNotIn -import assertk.assertions.isNotNull -import java.io.IOException -import java.net.InetAddress -import java.net.SocketTimeoutException -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit.SECONDS -import okhttp3.Call -import okhttp3.Connection -import okhttp3.EventListener -import okhttp3.OkHttpClient -import okhttp3.Request -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assumptions.abort -import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Tag -import org.junit.jupiter.api.Test - -/** - * Confirms a call pinned to a [Network] really goes over it, by checking the connected socket's - * local address against that network's own link addresses. - * - * Pinning goes through [AndroidNetworkPinning], which needs no more than API 23 here — unlike ECH, - * which needs 37. - * - * ## Typical Emulator - * - * ``` - * $ adb shell "dumpsys connectivity | grep -E 'Active default network|network\{10[01]\} handle|Requests:'" - * Active default network: 100 - * NetworkAgentInfo{network{100} handle{432902426637} ni{WIFI CONNECTED ... - * Requests: REQUEST:35 LISTEN:40 BACKGROUND_REQUEST:0 total:75 - * NetworkAgentInfo{network{101} handle{437197393933} ni{MOBILE[NR] CONNECTED ... - * Requests: REQUEST:0 LISTEN:12 BACKGROUND_REQUEST:1 total:13 - * ``` - * - * Network 100 is `wlan0` (10.0.2.16) and network 101 is `eth0` (10.0.2.15); both use NAT through the - * emulator host. `REQUEST:0` means nothing holds a request for it, AKA a *background* network. - * So need to request the network. - */ -@Tag("Remote") -class AndroidNetworksTest { - private val connectivityManager = - ApplicationProvider - .getApplicationContext() - .getSystemService(ConnectivityManager::class.java) - - private val networkCallbacks = mutableListOf() - - private lateinit var client: OkHttpClient - - @BeforeEach - fun setUp() { - // ConnectivityManager.getActiveNetwork() is API 23. Everything else here — getAllByName, - // getSocketFactory, requestNetwork, getLinkProperties — is API 21. - assumeTrue(Build.VERSION.SDK_INT >= 23) - - client = OkHttpClient() - } - - @AfterEach - fun tearDown() { - for (callback in networkCallbacks) { - connectivityManager.unregisterNetworkCallback(callback) - } - networkCallbacks.clear() - } - - /** The network the system would have picked anyway — network 100, Wi-Fi — named explicitly. */ - @Test - fun defaultNetworkCarriesTheCall() { - val network = connectivityManager.activeNetwork - assertThat(network).isNotNull() - - val capabilities = capabilitiesOf(network) - assertThat(capabilities?.hasCapability(NET_CAPABILITY_INTERNET)).isEqualTo(true) - assertThat(capabilities?.hasCapability(NET_CAPABILITY_VALIDATED)).isEqualTo(true) - - assertThat(localAddressOverNetwork(network!!)).isIn(*linkAddressesOf(network)) - } - - /** - * A network that is not the default one — network 101, cellular, on the emulator. The test skips - * on a device that only has the one network, but it does not skip if the system hands back the - * default: that would mean this ran twice over the same network while appearing to cover two. - */ - @Test - fun nonDefaultNetworkCarriesTheCall() { - val default = connectivityManager.activeNetwork - val (transport, transportName) = - when { - capabilitiesOf(default)?.hasTransport(TRANSPORT_CELLULAR) == true -> TRANSPORT_WIFI to "Wi-Fi" - else -> TRANSPORT_CELLULAR to "cellular" - } - - val network = requestNetwork(transport) - assumeTrue(network != null, "no $transportName network alongside the default one") - - assertThat(network).isNotEqualTo(default) - assertThat(capabilitiesOf(network)?.hasTransport(transport)).isEqualTo(true) - - val localAddress = localAddressOverNetwork(network!!) - assertThat(localAddress).isIn(*linkAddressesOf(network)) - // The point of the test: a different interface from the one the call would have taken. - assertThat(localAddress).isNotIn(*linkAddressesOf(default!!)) - } - - /** - * Makes a call pinned to [network] and returns the local address its socket ended up with. - * [AndroidNetworkPinning] pins both halves off the request tag: DNS and the socket factory. - * Without the socket factory a request resolved on one network still connects over the default - * one. - */ - private fun localAddressOverNetwork(network: Network): InetAddress? { - var localAddress: InetAddress? = null - - val networkClient = - client - .newBuilder() - .addInterceptor(AndroidNetworkPinning()) - .eventListener( - object : EventListener() { - override fun connectionAcquired( - call: Call, - connection: Connection, - ) { - localAddress = connection.socket().localAddress - } - }, - ).build() - - val request = - Request - .Builder() - .url(URL) - .tag(network) - .build() - - try { - networkClient.newCall(request).execute().use { response -> - assertThat(response.code).isEqualTo(200) - } - } catch (e: IOException) { - // Skipped flaky emulator rather than failed - if (e.isConnectTimeout()) { - abort("$network could not reach $URL: ${e.message}") - } - throw e - } - - return localAddress - } - - private fun Throwable.isConnectTimeout(): Boolean = - this is SocketTimeoutException || - cause?.isConnectTimeout() == true || - suppressed.any { it.isConnectTimeout() } - - /** - * Asks for a network on [transport] and returns it once it is available, or null if none turns up - * in time. The callback stays registered until [tearDown], because releasing the request is what - * would drop the network back into the background and break the calls made over it. - */ - private fun requestNetwork(transport: Int): Network? { - val request = - NetworkRequest - .Builder() - .addCapability(NET_CAPABILITY_INTERNET) - .addTransportType(transport) - .build() - - val available = LinkedBlockingQueue() - val callback = - object : NetworkCallback() { - override fun onAvailable(network: Network) { - available.offer(network) - } - } - - connectivityManager.requestNetwork(request, callback) - networkCallbacks += callback - - return available.poll(20, SECONDS) - } - - /** The addresses assigned to [network]'s interface, such as 10.0.2.16 for the emulator's Wi-Fi. */ - private fun linkAddressesOf(network: Network): Array = - connectivityManager - .getLinkProperties(network) - ?.linkAddresses - .orEmpty() - .map { it.address } - .toTypedArray() - - private fun capabilitiesOf(network: Network?): NetworkCapabilities? = connectivityManager.getNetworkCapabilities(network) - - companion object { - /** - * A single `A` record and no `AAAA`. Emulators are flaky for IPv6, avoid it. - */ - private const val URL = "https://publicobject.com/robots.txt" - } -} diff --git a/android-test/src/main/AndroidManifest.xml b/android-test/src/main/AndroidManifest.xml index c60b8afede9c..b0732f7778d6 100644 --- a/android-test/src/main/AndroidManifest.xml +++ b/android-test/src/main/AndroidManifest.xml @@ -3,8 +3,6 @@ - -