Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2025 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 okhttp.android.test

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.junit5.StartStop
import okhttp3.OkHttpClient
import okhttp3.OkHttpClientTestRule
import okhttp3.Request
import okhttp3.android.AndroidNetworkPinning
import okhttp3.internal.connection.RealCall
import okhttp3.internal.platform.PlatformRegistry
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
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
import org.junit.jupiter.api.extension.RegisterExtension

@Tag("Slow")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
class AndroidNetworkPinningTest {
@Suppress("RedundantVisibilityModifier")
@JvmField
@RegisterExtension
public val clientTestRule = OkHttpClientTestRule()

val applicationContext = ApplicationProvider.getApplicationContext<Context>()
val connectivityManager = applicationContext.getSystemService(ConnectivityManager::class.java)

val pinning = AndroidNetworkPinning()

private var client: OkHttpClient =
clientTestRule
.newClientBuilder()
.addCallDecorator(pinning)
.addCallDecorator {
it.proceed(
it.request
.newBuilder()
.header("second-decorator", "true")
.build(),
)
}.addInterceptor {
val call = (it.call() as RealCall)
val dns = call.client.dns
it
.proceed(it.request())
.newBuilder()
.header("used-dns", dns.javaClass.simpleName)
.build()
}.build()

@StartStop
private val server = MockWebServer()

@BeforeEach
fun setup() {
// Needed because of Platform.resetForTests
PlatformRegistry.applicationContext = applicationContext

connectivityManager.registerNetworkCallback(NetworkRequest.Builder().build(), pinning.networkCallback)
}

@Test
fun testDefaultRequest() {
server.enqueue(MockResponse(200, body = "Hello"))

val request = Request.Builder().url(server.url("/")).build()

val response = client.newCall(request).execute()

response.use {
assertEquals(200, response.code)
assertNotEquals("AndroidDns", response.header("used-dns"))
assertEquals("true", response.request.header("second-decorator"))
}
}

@Test
fun testPinnedRequest() {
server.enqueue(MockResponse(200, body = "Hello"))

val network = connectivityManager.activeNetwork

assumeTrue(network != null)

val request =
Request
.Builder()
.url(server.url("/"))
.tag<Network>(network)
.build()

val response = client.newCall(request).execute()

response.use {
assertEquals(200, response.code)
assertEquals("AndroidDns", response.header("used-dns"))
assertEquals("true", response.request.header("second-decorator"))
}
}
}
18 changes: 18 additions & 0 deletions okhttp/api/android/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable {
public abstract fun timeout ()Lokio/Timeout;
}

public abstract interface class okhttp3/Call$Chain {
public abstract fun getClient ()Lokhttp3/OkHttpClient;
public abstract fun getRequest ()Lokhttp3/Request;
public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call;
}

public abstract interface class okhttp3/Call$Decorator {
public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call;
}

public abstract interface class okhttp3/Call$Factory {
public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call;
}
Expand Down Expand Up @@ -902,6 +912,7 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact
public final fun fastFallback ()Z
public final fun followRedirects ()Z
public final fun followSslRedirects ()Z
public final fun getCallDecorators ()Ljava/util/List;
public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier;
public final fun interceptors ()Ljava/util/List;
public final fun minWebSocketMessageToCompress ()J
Expand All @@ -927,6 +938,7 @@ public final class okhttp3/OkHttpClient$Builder {
public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
public fun <init> ()V
public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder;
public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder;
Expand Down Expand Up @@ -1274,3 +1286,9 @@ public abstract class okhttp3/WebSocketListener {
public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V
}

public final class okhttp3/android/AndroidNetworkPinning : okhttp3/Call$Decorator {
public fun <init> ()V
public final fun getNetworkCallback ()Landroid/net/ConnectivityManager$NetworkCallback;
public fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call;
}

12 changes: 12 additions & 0 deletions okhttp/api/jvm/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable {
public abstract fun timeout ()Lokio/Timeout;
}

public abstract interface class okhttp3/Call$Chain {
public abstract fun getClient ()Lokhttp3/OkHttpClient;
public abstract fun getRequest ()Lokhttp3/Request;
public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call;
}

public abstract interface class okhttp3/Call$Decorator {
public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call;
}

public abstract interface class okhttp3/Call$Factory {
public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call;
}
Expand Down Expand Up @@ -901,6 +911,7 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact
public final fun fastFallback ()Z
public final fun followRedirects ()Z
public final fun followSslRedirects ()Z
public final fun getCallDecorators ()Ljava/util/List;
public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier;
public final fun interceptors ()Ljava/util/List;
public final fun minWebSocketMessageToCompress ()J
Expand All @@ -926,6 +937,7 @@ public final class okhttp3/OkHttpClient$Builder {
public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
public fun <init> ()V
public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder;
public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.ConnectivityManager
import android.net.Network
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.Collections
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.android.internal.AndroidDns
import okhttp3.internal.SuppressSignatureCheck

/**
* Decorator that supports Network Pinning on Android via Request tags.
*/
@RequiresApi(Build.VERSION_CODES.Q)
@SuppressSignatureCheck
class AndroidNetworkPinning : Call.Decorator {
private val pinnedClients = Collections.synchronizedMap(mutableMapOf<String, OkHttpClient>())

/** ConnectivityManager.NetworkCallback that will clean up after networks are lost. */
val networkCallback =
object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
pinnedClients.remove(network.toString())
}
}

override fun newCall(chain: Call.Chain): Call {
val request = chain.request

val pinnedNetwork = request.tag<Network>() ?: return chain.proceed(request)

val pinnedClient =
// API 24+
pinnedClients.computeIfAbsent(pinnedNetwork.toString()) {
chain.client.withNetwork(network = pinnedNetwork)
}

return pinnedClient.newCall(request)
}

private fun OkHttpClient.withNetwork(network: Network): OkHttpClient =
newBuilder()
.dns(AndroidDns(network))
.socketFactory(network.socketFactory)
.apply {
// Keep decorators after this one in the new client
callDecorators.subList(0, callDecorators.indexOf(this@AndroidNetworkPinning) + 1).clear()
}.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 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 android.net.DnsResolver
import android.net.Network
import android.os.Build
import androidx.annotation.RequiresApi
import java.net.InetAddress
import java.net.UnknownHostException
import java.util.concurrent.CompletableFuture
import okhttp3.Dns
import okhttp3.internal.SuppressSignatureCheck

@RequiresApi(Build.VERSION_CODES.Q)
@SuppressSignatureCheck
internal class AndroidDns(
val network: Network,
) : Dns {
// API 29+
private val dnsResolver = DnsResolver.getInstance()

override fun lookup(hostname: String): List<InetAddress> {
// API 24+
val result = CompletableFuture<List<InetAddress>>()

dnsResolver.query(
network,
hostname,
DnsResolver.FLAG_EMPTY,
{ it.run() },
null,
object : DnsResolver.Callback<List<InetAddress>> {
override fun onAnswer(
answer: List<InetAddress>,
rcode: Int,
) {
result.complete(answer)
}

override fun onError(error: DnsResolver.DnsException) {
result.completeExceptionally(
UnknownHostException(error.message).apply {
initCause(error)
},
)
}
},
)

return result.get()
}
}
30 changes: 30 additions & 0 deletions okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,34 @@ interface Call : Cloneable {
fun interface Factory {
fun newCall(request: Request): Call
}

/**
* The equivalent of an Interceptor for [Call.Factory], but critically supported within an [OkHttpClient].
*
* While an [Interceptor] forms a chain as part of execution of a Call. Call.Decorator intercepts
* [Call.Factory.newCall] with similar flexibility to Application [OkHttpClient.interceptors].
*
* That is, it may do any of
* - Modify the request such as adding Tracing Context
* - Wrap the [Call] returned
* - Return some [Call] implementation that will immediately fail avoiding network calls based on network or
* authentication state.
* - Redirect the [Call], such as using an alternative [Call.Factory].
* - Defer execution, something not safe in an Interceptor.
*
* It should not throw an exception and instead return a Call that will fail on [Call.execute].
*
* This flexibility means that the app developer configuring the decorators on [OkHttpClient] must be responsible
* for how these are composed in a chain.
*/
fun interface Decorator {
fun newCall(chain: Chain): Call
}

interface Chain {
val client: OkHttpClient
val request: Request

fun proceed(request: Request): Call
}
}
Loading
Loading