Skip to content
Draft
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
2 changes: 2 additions & 0 deletions dd-sdk-android-core/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ data class com.datadog.android.core.configuration.Configuration
fun setUploadSchedulerStrategy(UploadSchedulerStrategy?): Builder
fun setVersion(String): Builder
companion object
class com.datadog.android.core.configuration.HostPatternValidator
fun validate(List<String>, String): List<String>
class com.datadog.android.core.configuration.HostsSanitizer
fun sanitizeHosts(List<String>, String): List<String>
enum com.datadog.android.core.configuration.UploadFrequency
Expand Down
5 changes: 5 additions & 0 deletions dd-sdk-android-core/api/dd-sdk-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,11 @@ public final class com/datadog/android/core/configuration/Configuration$Builder
public final class com/datadog/android/core/configuration/Configuration$Companion {
}

public final class com/datadog/android/core/configuration/HostPatternValidator {
public fun <init> ()V
public final fun validate (Ljava/util/List;Ljava/lang/String;)Ljava/util/List;
}

public final class com/datadog/android/core/configuration/HostsSanitizer {
public fun <init> ()V
public final fun sanitizeHosts (Ljava/util/List;Ljava/lang/String;)Ljava/util/List;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.configuration

import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.utils.unboundInternalLogger
import com.datadog.android.lint.InternalApi
import java.util.Locale

/**
* Utility class to validate wildcard host patterns.
*
* A host pattern may contain at most one `*` wildcard and may only use the characters `a-z`, `0-9`,
* `.`, `-` and `*`. Plain host names (without a wildcard) are valid patterns too. This is the
* counterpart of [HostsSanitizer] for entries that are allowed to carry a wildcard.
*/
class HostPatternValidator {

/**
* Validates the given host patterns, returning the valid ones lowercased and in their original
* order. Entries containing characters outside `[a-z0-9.*-]` or more than one `*` wildcard are
* dropped and a warning is logged.
*
* @param patterns Host patterns to validate.
* @param feature SDK feature requesting the validation.
*/
@InternalApi
fun validate(
patterns: List<String>,
feature: String
): List<String> {
return patterns.mapNotNull { validatePattern(it, feature) }
}

private fun validatePattern(pattern: String, feature: String): String? {
val lowercased = pattern.lowercase(Locale.US)
return when {
lowercased.contains(INVALID_CHARACTER) -> {
unboundInternalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.USER,
{ ERROR_INVALID_CHARACTERS.format(Locale.US, pattern, feature) }
)
null
}
lowercased.count { it == WILDCARD } > 1 -> {
unboundInternalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.USER,
{ ERROR_MULTIPLE_WILDCARDS.format(Locale.US, pattern, feature) }
)
null
}
else -> lowercased
Comment thread
kikoveiga marked this conversation as resolved.
}
}

internal companion object {
private const val WILDCARD = '*'

private val INVALID_CHARACTER = Regex("[^a-z0-9.*-]")

internal const val ERROR_INVALID_CHARACTERS: String =
"You are using a malformed host pattern \"%s\" to setup %s tracking. It will be dropped. " +
"A host pattern may only contain lowercase letters, digits, '.', '-' and a single '*' wildcard."

internal const val ERROR_MULTIPLE_WILDCARDS: String =
"You are using a host pattern \"%s\" with more than one wildcard to setup %s tracking. " +
"It will be dropped. A host pattern may contain at most one '*' wildcard."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* 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.configuration

import com.datadog.android.api.InternalLogger
import com.datadog.android.utils.config.InternalLoggerTestConfiguration
import com.datadog.tools.unit.annotations.TestConfigurationsProvider
import com.datadog.tools.unit.extensions.TestConfigurationExtension
import com.datadog.tools.unit.extensions.config.TestConfiguration
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.annotation.StringForgeryType
import fr.xgouchet.elmyr.junit5.ForgeExtension
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.junit.jupiter.MockitoExtension
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import java.util.Locale

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class),
ExtendWith(TestConfigurationExtension::class)
)
internal class HostPatternValidatorTest {

lateinit var testedValidator: HostPatternValidator

@StringForgery(type = StringForgeryType.ALPHABETICAL)
lateinit var fakeFeature: String

@BeforeEach
fun `set up`() {
testedValidator = HostPatternValidator()
}

@Test
fun `M keep valid wildcard patterns W validate()`() {
// Given
val patterns = listOf("*.example.com", "preview-*.shopist.io", "example.net")

// When
val result = testedValidator.validate(patterns, fakeFeature)

// Then
assertThat(result).isEqualTo(patterns)
}

@Test
fun `M keep plain hosts W validate { no wildcard }`(
@StringForgery(
regex = "(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)+([a-z]|[a-z][a-z0-9-]*[a-z0-9])"
) hosts: List<String>
) {
// When
val result = testedValidator.validate(hosts, fakeFeature)

// Then
assertThat(result).isEqualTo(hosts)
}

@Test
fun `M keep single wildcard W validate { star }`() {
// When
val result = testedValidator.validate(listOf("*"), fakeFeature)

// Then
assertThat(result).containsExactly("*")
}

@Test
fun `M keep empty string W validate { blank }`() {
// When
val result = testedValidator.validate(listOf(""), fakeFeature)

// Then
assertThat(result).containsExactly("")
}

@Test
fun `M lowercase patterns W validate { uppercase }`() {
// When
val result = testedValidator.validate(
listOf("*.SHOPIST.IO", "Preview-*.Example.COM"),
fakeFeature
)

// Then
assertThat(result).containsExactly("*.shopist.io", "preview-*.example.com")
}

@Test
fun `M drop and warn W validate { more than one wildcard }`() {
// Given
val pattern = "*.foo.*.bar"

// When
val result = testedValidator.validate(listOf(pattern), fakeFeature)

// Then
assertThat(result).isEmpty()
val expectedMessage = HostPatternValidator.ERROR_MULTIPLE_WILDCARDS.format(
Locale.US,
pattern,
fakeFeature
)
argumentCaptor<() -> String> {
verify(logger.mockInternalLogger).log(
eq(InternalLogger.Level.WARN),
eq(InternalLogger.Target.USER),
capture(),
isNull(),
eq(false),
eq(null)
)
assertThat(firstValue()).isEqualTo(expectedMessage)
}
}

@Test
fun `M drop and warn W validate { invalid characters }`() {
// Given
val patterns = listOf("foo'bar.com", "back\\slash.com", "https://foo.com")

// When
val result = testedValidator.validate(patterns, fakeFeature)

// Then
assertThat(result).isEmpty()
val expectedMessages = patterns.map {
HostPatternValidator.ERROR_INVALID_CHARACTERS.format(Locale.US, it, fakeFeature)
}
argumentCaptor<() -> String> {
verify(logger.mockInternalLogger, times(patterns.size)).log(
eq(InternalLogger.Level.WARN),
eq(InternalLogger.Target.USER),
capture(),
isNull(),
eq(false),
eq(null)
)
assertThat(allValues.map { it() })
.containsExactlyInAnyOrderElementsOf(expectedMessages)
}
}

@Test
fun `M warn with original pattern W validate { uppercase invalid characters }`() {
// Given
val pattern = "FOO'BAR.com"

// When
testedValidator.validate(listOf(pattern), fakeFeature)

// Then
val expectedMessage = HostPatternValidator.ERROR_INVALID_CHARACTERS.format(
Locale.US,
pattern,
fakeFeature
)
argumentCaptor<() -> String> {
verify(logger.mockInternalLogger).log(
eq(InternalLogger.Level.WARN),
eq(InternalLogger.Target.USER),
capture(),
isNull(),
eq(false),
eq(null)
)
assertThat(firstValue()).isEqualTo(expectedMessage)
}
}

@Test
fun `M keep valid and drop invalid W validate { mixed }`() {
// Given
val validWildcard = "*.shopist.io"
val plainHost = "example.com"
val multiWildcard = "*.foo.*.bar"
val invalidChars = "foo'bar.com"
val patterns = listOf(validWildcard, multiWildcard, plainHost, invalidChars)

// When
val result = testedValidator.validate(patterns, fakeFeature)

// Then
assertThat(result).containsExactly(validWildcard, plainHost)
}

companion object {
val logger = InternalLoggerTestConfiguration()

@TestConfigurationsProvider
@JvmStatic
fun getTestConfigurations(): List<TestConfiguration> {
return listOf(logger)
}
}
}
3 changes: 3 additions & 0 deletions detekt_custom_safe_calls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,7 @@ datadog:
- "kotlin.collections.List.mapNotNull(kotlin.Function1)"
- "kotlin.collections.List.maxOrNull()"
- "kotlin.collections.List.orEmpty()"
- "kotlin.collections.List.partition(kotlin.Function1)"
- "kotlin.collections.List.reduceOrNull(kotlin.Function2)"
- "kotlin.collections.List.reversed()"
- "kotlin.collections.List.shuffled()"
Expand Down Expand Up @@ -1223,10 +1224,12 @@ datadog:
- "kotlin.Any?.hashCode()"
- "kotlin.CharSequence.isNullOrEmpty()"
- "kotlin.String.all(kotlin.Function1)"
- "kotlin.String.any(kotlin.Function1)"
- "kotlin.String.codePoints()"
- "kotlin.String.constructor()"
- "kotlin.String.contains(kotlin.Char, kotlin.Boolean)"
- "kotlin.String.contains(kotlin.CharSequence, kotlin.Boolean)"
- "kotlin.String.contains(kotlin.text.Regex)"
- "kotlin.String.count(kotlin.Function1)"
- "kotlin.String.endsWith(kotlin.Char, kotlin.Boolean)"
- "kotlin.String.endsWith(kotlin.String, kotlin.Boolean)"
Expand Down
1 change: 1 addition & 0 deletions features/dd-sdk-android-webview/api/apiSurface
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
object com.datadog.android.webview.WebViewTracking
fun enable(android.webkit.WebView, List<String>, Float = 100f, com.datadog.android.api.SdkCore = Datadog.getInstance())
fun enableWithPatterns(android.webkit.WebView, List<String>, Float = 100f, com.datadog.android.api.SdkCore = Datadog.getInstance())
class _InternalWebViewProxy
constructor(com.datadog.android.api.SdkCore, String? = null)
fun consumeWebviewEvent(String)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ public final class com/datadog/android/webview/WebViewTracking {
public static final fun enable (Landroid/webkit/WebView;Ljava/util/List;F)V
public static final fun enable (Landroid/webkit/WebView;Ljava/util/List;FLcom/datadog/android/api/SdkCore;)V
public static synthetic fun enable$default (Landroid/webkit/WebView;Ljava/util/List;FLcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V
public static final fun enableWithPatterns (Landroid/webkit/WebView;Ljava/util/List;)V
public static final fun enableWithPatterns (Landroid/webkit/WebView;Ljava/util/List;F)V
public static final fun enableWithPatterns (Landroid/webkit/WebView;Ljava/util/List;FLcom/datadog/android/api/SdkCore;)V
public static synthetic fun enableWithPatterns$default (Landroid/webkit/WebView;Ljava/util/List;FLcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V
}

public final class com/datadog/android/webview/WebViewTracking$_InternalWebViewProxy {
Expand Down
Loading
Loading