From 2592f740bd5cc524bf3e0c91d511b1fdd4a40c5a Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 5 Feb 2025 18:52:36 +0100 Subject: [PATCH] Add algorithm pixels --- .../MaliciousSiteBlockerWebViewIntegration.kt | 23 +++++++++++++++++-- .../com/duckduckgo/app/pixels/AppPixelName.kt | 2 ++ .../build.gradle | 2 ++ .../impl/MaliciousSitePixelName.kt | 23 +++++++++++++++++++ .../impl/data/MaliciousSiteRepository.kt | 7 ++++++ ...liciousSiteProtectionRequestInterceptor.kt | 11 ++++++--- .../impl/data/network/MaliciousSiteService.kt | 10 ++++++++ 7 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/MaliciousSitePixelName.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt b/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt index bc166e120648..9e4c16788ef6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt @@ -24,8 +24,10 @@ import com.duckduckgo.app.browser.webview.ExemptedUrlsHolder.ExemptedUrl import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.di.IsMainProcess +import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature import com.duckduckgo.app.settings.db.SettingsDataStore +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection @@ -97,6 +99,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val exemptedUrlsHolder: ExemptedUrlsHolder, @IsMainProcess private val isMainProcess: Boolean, + private val pixel: Pixel, ) : MaliciousSiteBlockerWebViewIntegration, PrivacyConfigCallbackPlugin { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -161,19 +164,31 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( } val belongsToCurrentPage = documentUri?.host == request.requestHeaders["Referer"]?.toUri()?.host - if (request.isForMainFrame || (isForIframe(request) && belongsToCurrentPage)) { - when (val result = checkMaliciousUrl(decodedUrl, confirmationCallback)) { + val isForIframe = isForIframe(request) && belongsToCurrentPage + if (request.isForMainFrame || isForIframe) { + val result = checkMaliciousUrl(decodedUrl) { + if (isForIframe && it is Malicious) { + firePixelForMaliciousIframe(it.feed) + } + confirmationCallback(it) + } + when (result) { is ConfirmedResult -> { when (val status = result.status) { is Malicious -> { + if (isForIframe) { + firePixelForMaliciousIframe(status.feed) + } return IsMaliciousViewData.MaliciousSite(url, status.feed, false) } + is Safe -> { processedUrls.add(decodedUrl) return IsMaliciousViewData.Safe } } } + is WaitForConfirmation -> { processedUrls.add(decodedUrl) return IsMaliciousViewData.WaitForConfirmation @@ -231,6 +246,10 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( } } + private fun firePixelForMaliciousIframe(feed: Feed) { + pixel.fire(AppPixelName.MALICIOUS_SITE_DETECTED_IN_IFRAME, mapOf("category" to feed.name.lowercase())) + } + private suspend fun checkMaliciousUrl( url: String, confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit, diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index f909754f8e9e..df5cda775885 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -387,4 +387,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { DEDICATED_WEBVIEW_URL_EXTRACTION_FAILED("m_dedicated_webview_url_extraction_failed"), BLOCKLIST_TDS_FAILURE("blocklist_experiment_tds_download_failure"), + + MALICIOUS_SITE_DETECTED_IN_IFRAME("m_malicious-site-protection_iframe-loaded"), } diff --git a/malicious-site-protection/malicious-site-protection-impl/build.gradle b/malicious-site-protection/malicious-site-protection-impl/build.gradle index 7077945b75fa..111cb9f53dbe 100644 --- a/malicious-site-protection/malicious-site-protection-impl/build.gradle +++ b/malicious-site-protection/malicious-site-protection-impl/build.gradle @@ -54,6 +54,8 @@ dependencies { implementation Google.android.material + implementation project(path: ':statistics-api') + testImplementation AndroidX.test.ext.junit testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' testImplementation Testing.junit4 diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/MaliciousSitePixelName.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/MaliciousSitePixelName.kt new file mode 100644 index 000000000000..ebb1628e6a23 --- /dev/null +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/MaliciousSitePixelName.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * 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 com.duckduckgo.malicioussiteprotection.impl + +import com.duckduckgo.app.statistics.pixels.Pixel + +enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { + MALICIOUS_SITE_CLIENT_TIMEOUT("m_malicious-site-protection_client-timeout"), +} diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt index 494e9cbf8164..3d66b6c0d3d1 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt @@ -16,11 +16,13 @@ package com.duckduckgo.malicioussiteprotection.impl.data +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.MALWARE import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.PHISHING +import com.duckduckgo.malicioussiteprotection.impl.AppPixelName.MALICIOUS_SITE_CLIENT_TIMEOUT import com.duckduckgo.malicioussiteprotection.impl.data.db.MaliciousSiteDao import com.duckduckgo.malicioussiteprotection.impl.data.db.RevisionEntity import com.duckduckgo.malicioussiteprotection.impl.data.network.FilterResponse @@ -41,6 +43,7 @@ import com.duckduckgo.malicioussiteprotection.impl.models.Type.FILTER_SET import com.duckduckgo.malicioussiteprotection.impl.models.Type.HASH_PREFIXES import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn +import java.util.concurrent.TimeoutException import javax.inject.Inject import kotlinx.coroutines.withContext @@ -58,6 +61,7 @@ class RealMaliciousSiteRepository @Inject constructor( private val maliciousSiteDao: MaliciousSiteDao, private val maliciousSiteService: MaliciousSiteService, private val dispatcherProvider: DispatcherProvider, + private val pixels: Pixel, ) : MaliciousSiteRepository { override suspend fun containsHashPrefix(hashPrefix: String): Boolean { @@ -91,6 +95,9 @@ class RealMaliciousSiteRepository @Inject constructor( null } } + } catch (e: TimeoutException) { + pixels.fire(MALICIOUS_SITE_CLIENT_TIMEOUT) + listOf() } catch (e: Exception) { listOf() } diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteProtectionRequestInterceptor.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteProtectionRequestInterceptor.kt index bfaa449e5b21..132b7dea8199 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteProtectionRequestInterceptor.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteProtectionRequestInterceptor.kt @@ -37,9 +37,14 @@ class MaliciousSiteProtectionRequestInterceptor @Inject constructor() : ApiInter override fun intercept(chain: Chain): Response { val request = chain.request() - val authRequired = chain.request().tag(Invocation::class.java) - ?.method() - ?.isAnnotationPresent(AuthRequired::class.java) == true + val method = chain.request().tag(Invocation::class.java)?.method() + + val authRequired = method?.isAnnotationPresent(AuthRequired::class.java) == true + + val timeoutAvailable = method?.isAnnotationPresent(Timeout::class.java) == true + if (timeoutAvailable) { + method?.getAnnotation(Timeout::class.java)?.let { chain.call().timeout().timeout(it.duration, it.unit) } + } return if (authRequired) { val newRequest = chain.request().newBuilder() diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt index dfc5bd068149..b0fb4dd52e31 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt @@ -20,6 +20,8 @@ import com.duckduckgo.anvil.annotations.ContributesServiceApi import com.duckduckgo.common.utils.AppUrl.Url.API import com.duckduckgo.di.scopes.AppScope import com.squareup.moshi.Json +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeUnit.MILLISECONDS import retrofit2.http.GET import retrofit2.http.Query @@ -48,6 +50,7 @@ interface MaliciousSiteService { @GET("$BASE_URL$FILTER_SET_PATH?$CATEGORY=$MALWARE") suspend fun getMalwareFilterSet(@Query("revision") revision: Int): FilterSetResponse + @Timeout(1000, MILLISECONDS) @AuthRequired @GET("$BASE_URL/matches") suspend fun getMatches(@Query("hashPrefix") hashPrefix: String): MatchesResponse @@ -99,3 +102,10 @@ data class RevisionResponse( @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class AuthRequired + +/** + * This annotation is used in interceptors to be able to intercept the annotated service calls + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Timeout(val duration: Long, val unit: TimeUnit)