From 62b48cf3737997aa81c80b94021b860e7fb9d841 Mon Sep 17 00:00:00 2001 From: Xavier Gouchet Date: Thu, 21 Jul 2022 09:26:44 +0200 Subject: [PATCH] RUMM-2327 add internal DNS resolver --- .../android/glide/DatadogGlideModule.kt | 1 + .../android/core/internal/CoreFeature.kt | 7 ++ .../core/internal/net/RotatingDnsResolver.kt | 69 +++++++++++ .../internal/net/RotatingDnsResolverTest.kt | 117 ++++++++++++++++++ detekt.yml | 13 +- 5 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolver.kt create mode 100644 dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolverTest.kt diff --git a/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt b/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt index e820f85929..d3012d3b4c 100644 --- a/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt +++ b/dd-sdk-android-glide/src/main/kotlin/com/datadog/android/glide/DatadogGlideModule.kt @@ -78,6 +78,7 @@ open class DatadogGlideModule * and [DatadogEventListener.Factory]. * @return the builder for the [OkHttpClient] to be used by Glide */ + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here open fun getClientBuilder(): OkHttpClient.Builder { return OkHttpClient.Builder() .addInterceptor(DatadogInterceptor(firstPartyHosts, traceSamplingRate = samplingRate)) diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index 1d17963a42..7dcc50305e 100644 --- a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -20,6 +20,7 @@ import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.core.internal.net.CurlInterceptor import com.datadog.android.core.internal.net.FirstPartyHostDetector import com.datadog.android.core.internal.net.GzipRequestInterceptor +import com.datadog.android.core.internal.net.RotatingDnsResolver import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider import com.datadog.android.core.internal.net.info.NetworkInfoDeserializer @@ -400,8 +401,10 @@ internal object CoreFeature { .connectionSpecs(listOf(connectionSpec)) if (BuildConfig.DEBUG) { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here builder.addNetworkInterceptor(CurlInterceptor()) } else { + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here builder.addInterceptor(GzipRequestInterceptor()) } @@ -409,6 +412,10 @@ internal object CoreFeature { builder.proxy(configuration.proxy) builder.proxyAuthenticator(configuration.proxyAuth) } + + @Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here + builder.dns(RotatingDnsResolver()) + okHttpClient = builder.build() } diff --git a/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolver.kt b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolver.kt new file mode 100644 index 0000000000..9e2e0f2946 --- /dev/null +++ b/dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolver.kt @@ -0,0 +1,69 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net + +import java.net.InetAddress +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.nanoseconds +import okhttp3.Dns + +internal class RotatingDnsResolver( + private val delegate: Dns = Dns.SYSTEM, + private val ttl: Duration = TTL_30_MIN +) : Dns { + + data class ResolvedHost( + val hostname: String, + val addresses: MutableList + ) { + private val resolutionTimestamp: Long = System.nanoTime() + + fun getAge(): Duration { + return (System.nanoTime() - resolutionTimestamp).nanoseconds + } + + fun rotate() { + val first = addresses.removeFirstOrNull() + if (first != null) { + addresses.add(first) + } + } + } + + private val knownHosts = mutableMapOf() + + // region Dns + + override fun lookup(hostname: String): MutableList { + val knownHost = knownHosts[hostname] + + return if (knownHost != null && isValid(knownHost)) { + knownHost.rotate() + knownHost.addresses.toMutableList() + } else { + @Suppress("UnsafeThirdPartyFunctionCall") // handled by caller + val result = delegate.lookup(hostname) + knownHosts[hostname] = ResolvedHost(hostname, result.toMutableList()) + result + } + } + + // endregion + + // region Internal + + private fun isValid(knownHost: ResolvedHost): Boolean { + return knownHost.getAge() < ttl && knownHost.addresses.isNotEmpty() + } + + // endregion + + companion object { + val TTL_30_MIN: Duration = 30.minutes + } +} diff --git a/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolverTest.kt b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolverTest.kt new file mode 100644 index 0000000000..91c4e4d793 --- /dev/null +++ b/dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/net/RotatingDnsResolverTest.kt @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.net + +import com.datadog.android.utils.forge.Configurator +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import java.net.InetAddress +import kotlin.time.Duration.Companion.milliseconds +import okhttp3.Dns +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class RotatingDnsResolverTest { + + lateinit var testedDns: Dns + + @Mock + lateinit var mockDelegate: Dns + + @StringForgery + lateinit var fakeHostname: String + + lateinit var fakeInetAddresses: List + + @BeforeEach + fun `set up`(forge: Forge) { + fakeInetAddresses = forge.aList { mock() } + + testedDns = RotatingDnsResolver(mockDelegate, TEST_TTL_MS) + } + + @Test + fun `𝕄 return delegate result 𝕎 lookup {unknown hostname}`() { + // Given + whenever(mockDelegate.lookup(fakeHostname)) doReturn fakeInetAddresses + + // When + val result = testedDns.lookup(fakeHostname) + + // Then + assertThat(result).containsExactlyElementsOf(fakeInetAddresses) + } + + @Test + fun `𝕄 rotate known result 𝕎 lookup {known hostname}`() { + // Given + whenever(mockDelegate.lookup(fakeHostname)) doReturn fakeInetAddresses + val result = mutableListOf() + + // When + for (i in fakeInetAddresses.indices) { + result.add(testedDns.lookup(fakeHostname).first()) + } + + // Then + assertThat(result).containsExactlyElementsOf(fakeInetAddresses) + } + + @Test + fun `𝕄 renew result 𝕎 lookup {expired hostname}`( + forge: Forge + ) { + // Given + val fakeInetAddresses2: List = forge.aList { mock() } + whenever(mockDelegate.lookup(fakeHostname)).doReturn(fakeInetAddresses, fakeInetAddresses2) + + // When + val result = testedDns.lookup(fakeHostname) + Thread.sleep(TEST_TTL_MS.inWholeMilliseconds) + val result2 = testedDns.lookup(fakeHostname) + + // Then + assertThat(result).containsExactlyElementsOf(fakeInetAddresses) + assertThat(result2).containsExactlyElementsOf(fakeInetAddresses2) + } + + @Test + fun `𝕄 renew result 𝕎 lookup {empty hostname}`() { + // Given + whenever(mockDelegate.lookup(fakeHostname)).doReturn(emptyList(), fakeInetAddresses) + + // When + val result = testedDns.lookup(fakeHostname) + val result2 = testedDns.lookup(fakeHostname) + + // Then + assertThat(result).isEmpty() + assertThat(result2).containsExactlyElementsOf(fakeInetAddresses) + } + + companion object { + internal val TEST_TTL_MS = 250.milliseconds + } +} diff --git a/detekt.yml b/detekt.yml index 4c59852624..1c9647c127 100644 --- a/detekt.yml +++ b/detekt.yml @@ -689,6 +689,7 @@ datadog: # endregion # region OkHttp - "okhttp3.Call.execute():java.io.IOException" + - "okhttp3.Dns.lookup(kotlin.String):java.net.UnknownHostException" - "okhttp3.Response.peekBody(kotlin.Long):java.io.IOException" - "okhttp3.RequestBody.writeTo(okio.BufferedSink):java.io.IOException" - "okhttp3.Response.close():java.lang.IllegalStateException" @@ -699,6 +700,10 @@ datadog: - "okhttp3.Request.Builder.url(kotlin.String):java.lang.NullPointerException,java.lang.IllegalArgumentException" - "okhttp3.Interceptor.Chain.proceed(okhttp3.Request):java.io.IOException" - "okhttp3.RequestBody.writeTo(okio.BufferedSink):java.io.IOException" + - "okhttp3.OkHttpClient.Builder.addInterceptor(okhttp3.Interceptor):java.lang.IllegalArgumentException" + - "okhttp3.OkHttpClient.Builder.addNetworkInterceptor(okhttp3.Interceptor):java.lang.IllegalArgumentException" + - "okhttp3.OkHttpClient.Builder.dns(okhttp3.Dns):java.lang.IllegalArgumentException" + - "okhttp3.OkHttpClient.Builder.eventListenerFactory(okhttp3.EventListener.Factory):java.lang.NullPointerException" - "okio.BufferedSink.close():java.io.IOException" - "okio.Okio.buffer(okio.Sink):java.lang.NullPointerException" - "okio.Buffer.readString(java.nio.charset.Charset):java.lang.IllegalArgumentException,java.io.IOException" @@ -1019,6 +1024,7 @@ datadog: - "kotlin.collections.MutableList.add(java.lang.ref.WeakReference)" - "kotlin.collections.MutableList.add(kotlin.ByteArray)" - "kotlin.collections.MutableList.add(kotlin.String)" + - "kotlin.collections.MutableList.add(java.net.InetAddress)" - "kotlin.collections.MutableList.addAll(kotlin.collections.Collection)" - "kotlin.collections.MutableList.clear()" - "kotlin.collections.MutableList.count(kotlin.Function1)" @@ -1026,13 +1032,16 @@ datadog: - "kotlin.collections.MutableList.firstOrNull(kotlin.Function1)" - "kotlin.collections.MutableList.forEach(kotlin.Function1)" - "kotlin.collections.MutableList.isEmpty()" + - "kotlin.collections.MutableList.isNotEmpty()" - "kotlin.collections.MutableList.iterator()" - "kotlin.collections.MutableList.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)" - "kotlin.collections.MutableList.remove(java.io.File)" - "kotlin.collections.MutableList.remove(java.lang.ref.WeakReference)" - "kotlin.collections.MutableList.removeAll(kotlin.Function1)" + - "kotlin.collections.MutableList.removeFirstOrNull()" - "kotlin.collections.MutableList.toSet()" - "kotlin.collections.MutableList.toTypedArray()" + - "kotlin.collections.MutableList.toMutableList()" - "kotlin.collections.MutableList.withIndex()" - "kotlin.collections.MutableList?.firstOrNull(kotlin.Function1)" - "kotlin.collections.MutableMap.forEach(kotlin.Function1)" @@ -1122,6 +1131,7 @@ datadog: - "kotlin.Throwable.stackTraceToString()" - "kotlin.text.Regex.constructor(kotlin.String)" - "kotlin.text.Regex.matchEntire(kotlin.CharSequence)" + # endregion # region Kotlin Coroutines - "kotlinx.coroutines.CoroutineScope.async(kotlin.coroutines.CoroutineContext, kotlinx.coroutines.CoroutineStart, kotlin.coroutines.SuspendFunction1)" - "kotlinx.coroutines.CoroutineScope.launch(kotlin.coroutines.CoroutineContext, kotlinx.coroutines.CoroutineStart, kotlin.coroutines.SuspendFunction1)" @@ -1163,13 +1173,10 @@ datadog: - "okhttp3.HttpUrl.url()" - "okhttp3.Interceptor.Chain.request()" - "okhttp3.OkHttpClient.Builder()" - - "okhttp3.OkHttpClient.Builder.addInterceptor(okhttp3.Interceptor)" - - "okhttp3.OkHttpClient.Builder.addNetworkInterceptor(okhttp3.Interceptor)" - "okhttp3.OkHttpClient.Builder.build()" - "okhttp3.OkHttpClient.Builder.callTimeout(kotlin.Long, java.util.concurrent.TimeUnit)" - "okhttp3.OkHttpClient.Builder.connectionSpecs(kotlin.collections.MutableList)" - "okhttp3.OkHttpClient.Builder.constructor()" - - "okhttp3.OkHttpClient.Builder.eventListenerFactory(okhttp3.EventListener.Factory)" - "okhttp3.OkHttpClient.Builder.protocols(kotlin.collections.MutableList)" - "okhttp3.OkHttpClient.Builder.proxy(java.net.Proxy)" - "okhttp3.OkHttpClient.Builder.proxyAuthenticator(okhttp3.Authenticator)"